Merge pull request 'fixes/mobile-comparator' (#25) from fixes/mobile-comparator into main
Reviewed-on: #25
This commit was merged in pull request #25.
This commit is contained in:
@@ -49,8 +49,6 @@ onMount(async () => {
|
||||
}
|
||||
fontsReady = true;
|
||||
});
|
||||
|
||||
$inspect(fontsReady);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
@@ -112,7 +112,7 @@ export class AppliedFontsManager {
|
||||
const internalName = `f_${config.id}`;
|
||||
const weightRange = config.isVariable ? '100 900' : `${config.weight}`;
|
||||
|
||||
const font = new FontFace(config.name, `url(${config.url})`, {
|
||||
const font = new FontFace(config.name, `url(${config.url}) format('woff2')`, {
|
||||
weight: weightRange,
|
||||
style: 'normal',
|
||||
display: 'swap',
|
||||
|
||||
@@ -6,30 +6,24 @@
|
||||
- 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';
|
||||
import { appliedFontsManager } from '../../model';
|
||||
import {
|
||||
type UnifiedFont,
|
||||
appliedFontsManager,
|
||||
} from '../../model';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Font name to set
|
||||
* Applied font
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Font id to load
|
||||
*/
|
||||
id: string;
|
||||
/** */
|
||||
url: string;
|
||||
font: UnifiedFont;
|
||||
/**
|
||||
* Font weight
|
||||
*/
|
||||
weight?: number;
|
||||
/**
|
||||
* Variable font flag
|
||||
*/
|
||||
isVariable?: boolean;
|
||||
/**
|
||||
* Additional classes
|
||||
*/
|
||||
@@ -40,12 +34,12 @@ interface Props {
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { name, id, url, weight = 400, isVariable = false, className, children }: Props = $props();
|
||||
let { font, weight = 400, 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));
|
||||
const status = $derived(appliedFontsManager.getFontStatus(font.id, weight, font.features.isVariable));
|
||||
|
||||
$effect(() => {
|
||||
if (status === 'loaded' || status === 'error') {
|
||||
@@ -56,17 +50,19 @@ $effect(() => {
|
||||
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,
|
||||
id: font.id,
|
||||
weight,
|
||||
name,
|
||||
name: font.name,
|
||||
url,
|
||||
isVariable,
|
||||
isVariable: font.features.isVariable,
|
||||
}]);
|
||||
}
|
||||
|
||||
observer.unobserve(element);
|
||||
}
|
||||
@@ -88,7 +84,7 @@ const transitionClasses = $derived(
|
||||
|
||||
<div
|
||||
bind:this={element}
|
||||
style:font-family={shouldReveal ? `'${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
|
||||
|
||||
@@ -84,8 +84,7 @@ const letterSpacing = $derived(controlManager.spacing);
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-5 md:p-8 relative z-10">
|
||||
<!-- TODO: Fix this ! -->
|
||||
<FontApplicator id={font.id} name={font.name} url={font.styles.regular!}>
|
||||
<FontApplicator {font} weight={fontWeight}>
|
||||
<ContentEditable
|
||||
bind:text={text}
|
||||
{...restProps}
|
||||
|
||||
@@ -109,7 +109,7 @@ function calculateScale(index: number): number | string {
|
||||
{#snippet ComboControl()}
|
||||
<div
|
||||
class={cn(
|
||||
'flex gap-4 sm:p-4 rounded-xl transition-all duration-300',
|
||||
'flex gap-4 sm:py-4 sm:px-1 rounded-xl transition-all duration-300',
|
||||
'backdrop-blur-md',
|
||||
orientation === 'horizontal' ? 'flex-row items-end w-full' : 'flex-col items-center h-full',
|
||||
className,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Animated wrapper for content that can be expanded and collapsed.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { debounce } from '$shared/lib/utils';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
@@ -38,6 +39,10 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
* Optional badge to render
|
||||
*/
|
||||
badge?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
|
||||
/**
|
||||
* Callback for when the element's size changes
|
||||
*/
|
||||
onResize?: (rect: DOMRectReadOnly) => void;
|
||||
/**
|
||||
* Rotation animation direction
|
||||
* @default 'clockwise'
|
||||
@@ -56,6 +61,7 @@ let {
|
||||
visibleContent,
|
||||
hiddenContent,
|
||||
badge,
|
||||
onResize,
|
||||
rotation = 'clockwise',
|
||||
class: className = '',
|
||||
containerClassName = '',
|
||||
@@ -64,7 +70,7 @@ let {
|
||||
|
||||
let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
export const xSpring = new Spring(0, {
|
||||
const xSpring = new Spring(0, {
|
||||
stiffness: 0.14, // Lower is slower
|
||||
damping: 0.5, // Settle
|
||||
});
|
||||
@@ -79,7 +85,7 @@ const scaleSpring = new Spring(1, {
|
||||
damping: 0.65,
|
||||
});
|
||||
|
||||
export const rotateSpring = new Spring(0, {
|
||||
const rotateSpring = new Spring(0, {
|
||||
stiffness: 0.12,
|
||||
damping: 0.55,
|
||||
});
|
||||
@@ -107,6 +113,9 @@ function handleKeyDown(e: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// Create debounced recize callback
|
||||
const debouncedResize = debounce((entry: ResizeObserverEntry) => onResize?.(entry.contentRect), 50);
|
||||
|
||||
// Elevation and scale on activation
|
||||
$effect(() => {
|
||||
if (expanded && !disabled) {
|
||||
@@ -149,6 +158,21 @@ $effect(() => {
|
||||
expanded = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Use an effect to watch the element's actual physical size
|
||||
$effect(() => {
|
||||
if (!element) return;
|
||||
|
||||
const observer = new ResizeObserver(entries => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
debouncedResize(entry);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -158,7 +182,7 @@ $effect(() => {
|
||||
role="button"
|
||||
tabindex={0}
|
||||
class={cn(
|
||||
'will-change-transform duration-300',
|
||||
'will-change-[transform, width, height] duration-300',
|
||||
disabled ? 'pointer-events-none' : 'pointer-events-auto',
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -34,6 +34,7 @@ class ComparisonStore {
|
||||
#fontB = $state<UnifiedFont | undefined>();
|
||||
#sampleText = $state('The quick brown fox jumps over the lazy dog');
|
||||
#isRestoring = $state(true);
|
||||
#fontsReady = $state(false);
|
||||
#typography = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA, 'glyphdiff:comparison:typography');
|
||||
|
||||
constructor() {
|
||||
@@ -49,6 +50,7 @@ class ComparisonStore {
|
||||
|
||||
// If we already have a selection, do nothing
|
||||
if (this.#fontA && this.#fontB) {
|
||||
this.#checkFontsLoaded();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,6 +68,48 @@ class ComparisonStore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if fonts are actually loaded in the browser at current weight.
|
||||
* Uses CSS Font Loading API to prevent FOUT.
|
||||
*/
|
||||
async #checkFontsLoaded() {
|
||||
if (!('fonts' in document)) {
|
||||
this.#fontsReady = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.#fontsReady = false;
|
||||
|
||||
const weight = this.#typography.weight;
|
||||
const size = this.#typography.renderedSize;
|
||||
const fontAName = this.#fontA?.name;
|
||||
const fontBName = this.#fontB?.name;
|
||||
|
||||
if (!fontAName || !fontBName) return;
|
||||
|
||||
try {
|
||||
// Step 1: Load fonts into memory
|
||||
await Promise.all([
|
||||
document.fonts.load(`${weight} ${size}px "${fontAName}"`),
|
||||
document.fonts.load(`${weight} ${size}px "${fontBName}"`),
|
||||
]);
|
||||
|
||||
// Step 2: Wait for browser to be ready to render
|
||||
await document.fonts.ready;
|
||||
|
||||
// Step 3: Force a layout/paint cycle (critical!)
|
||||
await new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(resolve); // Double rAF ensures paint completes
|
||||
});
|
||||
});
|
||||
|
||||
this.#fontsReady = true;
|
||||
} catch (error) {
|
||||
console.warn('[ComparisonStore] Font loading failed:', error);
|
||||
setTimeout(() => this.#fontsReady = true, 1000);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Restore state from persistent storage
|
||||
*/
|
||||
@@ -141,13 +185,12 @@ class ComparisonStore {
|
||||
* Check if both fonts are selected
|
||||
*/
|
||||
get isReady() {
|
||||
return !!this.#fontA && !!this.#fontB;
|
||||
return !!this.#fontA && !!this.#fontB && this.#fontsReady;
|
||||
}
|
||||
|
||||
get isLoading() {
|
||||
return this.#isRestoring;
|
||||
return this.#isRestoring || !this.#fontsReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public initializer (optional, as constructor starts it)
|
||||
* Kept for compatibility if manual re-init is needed
|
||||
|
||||
@@ -167,10 +167,6 @@ $effect(() => {
|
||||
{char}
|
||||
{proximity}
|
||||
{isPast}
|
||||
weight={typography.weight}
|
||||
size={typography.renderedSize}
|
||||
fontAName={fontA.name}
|
||||
fontBName={fontB.name}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -202,7 +198,9 @@ $effect(() => {
|
||||
>
|
||||
<!-- Text Rendering Container -->
|
||||
{#if isLoading}
|
||||
<div out:fade={{ duration: 300 }}>
|
||||
<Loader size={24} />
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
@@ -18,35 +21,51 @@ interface Props {
|
||||
* Flag indicating whether character needed to be changed
|
||||
*/
|
||||
isPast: boolean;
|
||||
/**
|
||||
* Font weight of the character
|
||||
*/
|
||||
weight: number;
|
||||
/**
|
||||
* Font size of the character
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* Name of the font for the character after the change
|
||||
*/
|
||||
fontAName: string;
|
||||
/**
|
||||
* Name of the font for the character before the change
|
||||
*/
|
||||
fontBName: string;
|
||||
}
|
||||
|
||||
let { char, proximity, isPast, weight, size, fontAName, fontBName }: Props = $props();
|
||||
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}
|
||||
<span
|
||||
class={cn(
|
||||
'inline-block transition-all duration-300 ease-out will-change-transform',
|
||||
isPast ? 'text-indigo-500' : 'text-neutral-950',
|
||||
)}
|
||||
style:font-family={isPast ? fontBName : fontAName}
|
||||
style:font-weight={weight}
|
||||
style:font-size={`${size}px`}
|
||||
style:font-family={isPast ? fontB.name : fontA.name}
|
||||
style:font-weight={typography.weight}
|
||||
style:font-size={`${typography.renderedSize}px`}
|
||||
style:transform="
|
||||
scale({1 + proximity * 0.3})
|
||||
translateY({-proximity * 12}px)
|
||||
@@ -58,6 +77,7 @@ let { char, proximity, isPast, weight, size, fontAName, fontBName }: Props = $pr
|
||||
>
|
||||
{char === ' ' ? '\u00A0' : char}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
span {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { appliedFontsManager } from '$entities/Font';
|
||||
import { getFontUrl } from '$entities/Font/lib';
|
||||
import type { ResponsiveManager } from '$shared/lib';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import {
|
||||
@@ -23,8 +25,27 @@ let { sliderPos, isDragging, typographyControls = $bindable<HTMLDivElement | nul
|
||||
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');
|
||||
|
||||
$effect(() => {
|
||||
if (!fontA || !fontB) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fontAUrl = getFontUrl(fontA, weight);
|
||||
const fontBUrl = getFontUrl(fontB, weight);
|
||||
|
||||
if (!fontAUrl || !fontBUrl) {
|
||||
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 };
|
||||
|
||||
appliedFontsManager.touch([fontAConfig, fontBConfig]);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if responsive.isMobile}
|
||||
|
||||
@@ -70,7 +70,7 @@ function selectFontB(font: UnifiedFont) {
|
||||
)}
|
||||
>
|
||||
<div class="text-left flex-1 min-w-0">
|
||||
<FontApplicator name={font.name} id={font.id} {url}>
|
||||
<FontApplicator {font} weight={typography.weight}>
|
||||
{font.name}
|
||||
</FontApplicator>
|
||||
</div>
|
||||
@@ -95,9 +95,8 @@ function selectFontB(font: UnifiedFont) {
|
||||
onclick={handleClick}
|
||||
>
|
||||
<FontApplicator
|
||||
name={fontListItem.name}
|
||||
id={fontListItem.id}
|
||||
url={getFontUrl(fontListItem, typography.weight) ?? ''}
|
||||
font={fontListItem}
|
||||
weight={typography.weight}
|
||||
>
|
||||
{fontListItem.name}
|
||||
</FontApplicator>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
@@ -27,6 +29,7 @@ let { sliderPos, isDragging }: Props = $props();
|
||||
)}
|
||||
style:left="{sliderPos}%"
|
||||
style:will-change={isDragging ? 'left' : 'auto'}
|
||||
in:fade={{ duration: 300, delay: 150, easing: cubicOut }}
|
||||
>
|
||||
<!-- We use part of lucide cursor svg icon as a handle -->
|
||||
<svg
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { comparisonStore } from '$widgets/ComparisonSlider/model';
|
||||
import AArrowUP from '@lucide/svelte/icons/a-arrow-up';
|
||||
import { type Orientation } from 'bits-ui';
|
||||
import { untrack } from 'svelte';
|
||||
import { Spring } from 'svelte/motion';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
@@ -74,7 +75,10 @@ function handleInputFocus() {
|
||||
|
||||
// Movement Logic
|
||||
$effect(() => {
|
||||
if (containerWidth === 0 || panelWidth === 0 || staticPosition) return;
|
||||
if (containerWidth === 0 || panelWidth === 0 || staticPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sliderX = (sliderPos / 100) * containerWidth;
|
||||
const buffer = 40;
|
||||
const leftTrigger = margin + panelWidth + buffer;
|
||||
@@ -88,21 +92,29 @@ $effect(() => {
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const targetX = side === 'right' ? containerWidth - panelWidth - margin * 2 : 0;
|
||||
// Trigger only when side changes
|
||||
const currentSide = side;
|
||||
|
||||
untrack(() => {
|
||||
if (containerWidth > 0 && panelWidth > 0) {
|
||||
const targetX = currentSide === 'right'
|
||||
? containerWidth - panelWidth - margin * 2
|
||||
: 0;
|
||||
|
||||
// On side change set the position and the rotation
|
||||
xSpring.target = targetX;
|
||||
rotateSpring.target = side === 'right' ? 3.5 : -3.5;
|
||||
rotateSpring.target = currentSide === 'right' ? 3.5 : -3.5;
|
||||
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
rotateSpring.target = 0;
|
||||
}, 600);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user