fixes/mobile-comparator #25
@@ -49,8 +49,6 @@ onMount(async () => {
|
|||||||
}
|
}
|
||||||
fontsReady = true;
|
fontsReady = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
$inspect(fontsReady);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export class AppliedFontsManager {
|
|||||||
const internalName = `f_${config.id}`;
|
const internalName = `f_${config.id}`;
|
||||||
const weightRange = config.isVariable ? '100 900' : `${config.weight}`;
|
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,
|
weight: weightRange,
|
||||||
style: 'normal',
|
style: 'normal',
|
||||||
display: 'swap',
|
display: 'swap',
|
||||||
|
|||||||
@@ -6,30 +6,24 @@
|
|||||||
- Adds smooth transition when font appears
|
- Adds smooth transition when font appears
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getFontUrl } from '$entities/Font/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { prefersReducedMotion } from 'svelte/motion';
|
import { prefersReducedMotion } from 'svelte/motion';
|
||||||
import { appliedFontsManager } from '../../model';
|
import {
|
||||||
|
type UnifiedFont,
|
||||||
|
appliedFontsManager,
|
||||||
|
} from '../../model';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
* Font name to set
|
* Applied font
|
||||||
*/
|
*/
|
||||||
name: string;
|
font: UnifiedFont;
|
||||||
/**
|
|
||||||
* Font id to load
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/** */
|
|
||||||
url: string;
|
|
||||||
/**
|
/**
|
||||||
* Font weight
|
* Font weight
|
||||||
*/
|
*/
|
||||||
weight?: number;
|
weight?: number;
|
||||||
/**
|
|
||||||
* Variable font flag
|
|
||||||
*/
|
|
||||||
isVariable?: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Additional classes
|
* Additional classes
|
||||||
*/
|
*/
|
||||||
@@ -40,12 +34,12 @@ interface Props {
|
|||||||
children?: Snippet;
|
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;
|
let element: Element;
|
||||||
|
|
||||||
// Track if the user has actually scrolled this into view
|
// Track if the user has actually scrolled this into view
|
||||||
let hasEnteredViewport = $state(false);
|
let hasEnteredViewport = $state(false);
|
||||||
const status = $derived(appliedFontsManager.getFontStatus(id, weight, isVariable));
|
const status = $derived(appliedFontsManager.getFontStatus(font.id, weight, font.features.isVariable));
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (status === 'loaded' || status === 'error') {
|
if (status === 'loaded' || status === 'error') {
|
||||||
@@ -56,17 +50,19 @@ $effect(() => {
|
|||||||
const observer = new IntersectionObserver(entries => {
|
const observer = new IntersectionObserver(entries => {
|
||||||
if (entries[0].isIntersecting) {
|
if (entries[0].isIntersecting) {
|
||||||
hasEnteredViewport = true;
|
hasEnteredViewport = true;
|
||||||
|
const url = getFontUrl(font, weight);
|
||||||
// Touch ensures it's in the queue.
|
// Touch ensures it's in the queue.
|
||||||
// It's safe to call this even if VirtualList called it
|
// It's safe to call this even if VirtualList called it
|
||||||
// (Manager dedupes based on key)
|
// (Manager dedupes based on key)
|
||||||
|
if (url) {
|
||||||
appliedFontsManager.touch([{
|
appliedFontsManager.touch([{
|
||||||
id,
|
id: font.id,
|
||||||
weight,
|
weight,
|
||||||
name,
|
name: font.name,
|
||||||
url,
|
url,
|
||||||
isVariable,
|
isVariable: font.features.isVariable,
|
||||||
}]);
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
observer.unobserve(element);
|
observer.unobserve(element);
|
||||||
}
|
}
|
||||||
@@ -88,7 +84,7 @@ const transitionClasses = $derived(
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={element}
|
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(
|
class={cn(
|
||||||
transitionClasses,
|
transitionClasses,
|
||||||
// If reduced motion is on, we skip the transform/blur entirely
|
// If reduced motion is on, we skip the transform/blur entirely
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ const letterSpacing = $derived(controlManager.spacing);
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 sm:p-5 md:p-8 relative z-10">
|
<div class="p-4 sm:p-5 md:p-8 relative z-10">
|
||||||
<!-- TODO: Fix this ! -->
|
<FontApplicator {font} weight={fontWeight}>
|
||||||
<FontApplicator id={font.id} name={font.name} url={font.styles.regular!}>
|
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
bind:text={text}
|
bind:text={text}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ function calculateScale(index: number): number | string {
|
|||||||
{#snippet ComboControl()}
|
{#snippet ComboControl()}
|
||||||
<div
|
<div
|
||||||
class={cn(
|
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',
|
'backdrop-blur-md',
|
||||||
orientation === 'horizontal' ? 'flex-row items-end w-full' : 'flex-col items-center h-full',
|
orientation === 'horizontal' ? 'flex-row items-end w-full' : 'flex-col items-center h-full',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
Animated wrapper for content that can be expanded and collapsed.
|
Animated wrapper for content that can be expanded and collapsed.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { debounce } from '$shared/lib/utils';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
@@ -38,6 +39,10 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
|||||||
* Optional badge to render
|
* Optional badge to render
|
||||||
*/
|
*/
|
||||||
badge?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
|
badge?: Snippet<[{ expanded?: boolean; disabled?: boolean }]>;
|
||||||
|
/**
|
||||||
|
* Callback for when the element's size changes
|
||||||
|
*/
|
||||||
|
onResize?: (rect: DOMRectReadOnly) => void;
|
||||||
/**
|
/**
|
||||||
* Rotation animation direction
|
* Rotation animation direction
|
||||||
* @default 'clockwise'
|
* @default 'clockwise'
|
||||||
@@ -56,6 +61,7 @@ let {
|
|||||||
visibleContent,
|
visibleContent,
|
||||||
hiddenContent,
|
hiddenContent,
|
||||||
badge,
|
badge,
|
||||||
|
onResize,
|
||||||
rotation = 'clockwise',
|
rotation = 'clockwise',
|
||||||
class: className = '',
|
class: className = '',
|
||||||
containerClassName = '',
|
containerClassName = '',
|
||||||
@@ -64,7 +70,7 @@ let {
|
|||||||
|
|
||||||
let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);
|
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
|
stiffness: 0.14, // Lower is slower
|
||||||
damping: 0.5, // Settle
|
damping: 0.5, // Settle
|
||||||
});
|
});
|
||||||
@@ -79,7 +85,7 @@ const scaleSpring = new Spring(1, {
|
|||||||
damping: 0.65,
|
damping: 0.65,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const rotateSpring = new Spring(0, {
|
const rotateSpring = new Spring(0, {
|
||||||
stiffness: 0.12,
|
stiffness: 0.12,
|
||||||
damping: 0.55,
|
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
|
// Elevation and scale on activation
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (expanded && !disabled) {
|
if (expanded && !disabled) {
|
||||||
@@ -149,6 +158,21 @@ $effect(() => {
|
|||||||
expanded = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -158,7 +182,7 @@ $effect(() => {
|
|||||||
role="button"
|
role="button"
|
||||||
tabindex={0}
|
tabindex={0}
|
||||||
class={cn(
|
class={cn(
|
||||||
'will-change-transform duration-300',
|
'will-change-[transform, width, height] duration-300',
|
||||||
disabled ? 'pointer-events-none' : 'pointer-events-auto',
|
disabled ? 'pointer-events-none' : 'pointer-events-auto',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class ComparisonStore {
|
|||||||
#fontB = $state<UnifiedFont | undefined>();
|
#fontB = $state<UnifiedFont | undefined>();
|
||||||
#sampleText = $state('The quick brown fox jumps over the lazy dog');
|
#sampleText = $state('The quick brown fox jumps over the lazy dog');
|
||||||
#isRestoring = $state(true);
|
#isRestoring = $state(true);
|
||||||
|
#fontsReady = $state(false);
|
||||||
#typography = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA, 'glyphdiff:comparison:typography');
|
#typography = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA, 'glyphdiff:comparison:typography');
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -49,6 +50,7 @@ class ComparisonStore {
|
|||||||
|
|
||||||
// If we already have a selection, do nothing
|
// If we already have a selection, do nothing
|
||||||
if (this.#fontA && this.#fontB) {
|
if (this.#fontA && this.#fontB) {
|
||||||
|
this.#checkFontsLoaded();
|
||||||
return;
|
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
|
* Restore state from persistent storage
|
||||||
*/
|
*/
|
||||||
@@ -141,13 +185,12 @@ class ComparisonStore {
|
|||||||
* Check if both fonts are selected
|
* Check if both fonts are selected
|
||||||
*/
|
*/
|
||||||
get isReady() {
|
get isReady() {
|
||||||
return !!this.#fontA && !!this.#fontB;
|
return !!this.#fontA && !!this.#fontB && this.#fontsReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isLoading() {
|
get isLoading() {
|
||||||
return this.#isRestoring;
|
return this.#isRestoring || !this.#fontsReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public initializer (optional, as constructor starts it)
|
* Public initializer (optional, as constructor starts it)
|
||||||
* Kept for compatibility if manual re-init is needed
|
* Kept for compatibility if manual re-init is needed
|
||||||
|
|||||||
@@ -167,10 +167,6 @@ $effect(() => {
|
|||||||
{char}
|
{char}
|
||||||
{proximity}
|
{proximity}
|
||||||
{isPast}
|
{isPast}
|
||||||
weight={typography.weight}
|
|
||||||
size={typography.renderedSize}
|
|
||||||
fontAName={fontA.name}
|
|
||||||
fontBName={fontB.name}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
@@ -202,7 +198,9 @@ $effect(() => {
|
|||||||
>
|
>
|
||||||
<!-- Text Rendering Container -->
|
<!-- Text Rendering Container -->
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
|
<div out:fade={{ duration: 300 }}>
|
||||||
<Loader size={24} />
|
<Loader size={24} />
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="
|
class="
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
Renders a character with particular styling based on proximity, isPast, weight, fontAName, and fontBName.
|
Renders a character with particular styling based on proximity, isPast, weight, fontAName, and fontBName.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { appliedFontsManager } from '$entities/Font';
|
||||||
|
import { getFontUrl } from '$entities/Font/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
|
import { comparisonStore } from '../../../model';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -18,35 +21,51 @@ interface Props {
|
|||||||
* Flag indicating whether character needed to be changed
|
* Flag indicating whether character needed to be changed
|
||||||
*/
|
*/
|
||||||
isPast: boolean;
|
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>
|
</script>
|
||||||
|
|
||||||
|
{#if fontA && fontB}
|
||||||
<span
|
<span
|
||||||
class={cn(
|
class={cn(
|
||||||
'inline-block transition-all duration-300 ease-out will-change-transform',
|
'inline-block transition-all duration-300 ease-out will-change-transform',
|
||||||
isPast ? 'text-indigo-500' : 'text-neutral-950',
|
isPast ? 'text-indigo-500' : 'text-neutral-950',
|
||||||
)}
|
)}
|
||||||
style:font-family={isPast ? fontBName : fontAName}
|
style:font-family={isPast ? fontB.name : fontA.name}
|
||||||
style:font-weight={weight}
|
style:font-weight={typography.weight}
|
||||||
style:font-size={`${size}px`}
|
style:font-size={`${typography.renderedSize}px`}
|
||||||
style:transform="
|
style:transform="
|
||||||
scale({1 + proximity * 0.3})
|
scale({1 + proximity * 0.3})
|
||||||
translateY({-proximity * 12}px)
|
translateY({-proximity * 12}px)
|
||||||
@@ -58,6 +77,7 @@ let { char, proximity, isPast, weight, size, fontAName, fontBName }: Props = $pr
|
|||||||
>
|
>
|
||||||
{char === ' ' ? '\u00A0' : char}
|
{char === ' ' ? '\u00A0' : char}
|
||||||
</span>
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
span {
|
span {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { appliedFontsManager } from '$entities/Font';
|
||||||
|
import { getFontUrl } from '$entities/Font/lib';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
import {
|
import {
|
||||||
@@ -23,8 +25,27 @@ let { sliderPos, isDragging, typographyControls = $bindable<HTMLDivElement | nul
|
|||||||
const fontA = $derived(comparisonStore.fontA);
|
const fontA = $derived(comparisonStore.fontA);
|
||||||
const fontB = $derived(comparisonStore.fontB);
|
const fontB = $derived(comparisonStore.fontB);
|
||||||
const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady);
|
const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady);
|
||||||
|
const weight = $derived(comparisonStore.typography.weight);
|
||||||
|
|
||||||
const responsive = getContext<ResponsiveManager>('responsive');
|
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>
|
</script>
|
||||||
|
|
||||||
{#if responsive.isMobile}
|
{#if responsive.isMobile}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function selectFontB(font: UnifiedFont) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div class="text-left flex-1 min-w-0">
|
<div class="text-left flex-1 min-w-0">
|
||||||
<FontApplicator name={font.name} id={font.id} {url}>
|
<FontApplicator {font} weight={typography.weight}>
|
||||||
{font.name}
|
{font.name}
|
||||||
</FontApplicator>
|
</FontApplicator>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,9 +95,8 @@ function selectFontB(font: UnifiedFont) {
|
|||||||
onclick={handleClick}
|
onclick={handleClick}
|
||||||
>
|
>
|
||||||
<FontApplicator
|
<FontApplicator
|
||||||
name={fontListItem.name}
|
font={fontListItem}
|
||||||
id={fontListItem.id}
|
weight={typography.weight}
|
||||||
url={getFontUrl(fontListItem, typography.weight) ?? ''}
|
|
||||||
>
|
>
|
||||||
{fontListItem.name}
|
{fontListItem.name}
|
||||||
</FontApplicator>
|
</FontApplicator>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
|
import { cubicOut } from 'svelte/easing';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -27,6 +29,7 @@ let { sliderPos, isDragging }: Props = $props();
|
|||||||
)}
|
)}
|
||||||
style:left="{sliderPos}%"
|
style:left="{sliderPos}%"
|
||||||
style:will-change={isDragging ? 'left' : 'auto'}
|
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 -->
|
<!-- We use part of lucide cursor svg icon as a handle -->
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { comparisonStore } from '$widgets/ComparisonSlider/model';
|
import { comparisonStore } from '$widgets/ComparisonSlider/model';
|
||||||
import AArrowUP from '@lucide/svelte/icons/a-arrow-up';
|
import AArrowUP from '@lucide/svelte/icons/a-arrow-up';
|
||||||
import { type Orientation } from 'bits-ui';
|
import { type Orientation } from 'bits-ui';
|
||||||
|
import { untrack } from 'svelte';
|
||||||
import { Spring } from 'svelte/motion';
|
import { Spring } from 'svelte/motion';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
@@ -74,7 +75,10 @@ function handleInputFocus() {
|
|||||||
|
|
||||||
// Movement Logic
|
// Movement Logic
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (containerWidth === 0 || panelWidth === 0 || staticPosition) return;
|
if (containerWidth === 0 || panelWidth === 0 || staticPosition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sliderX = (sliderPos / 100) * containerWidth;
|
const sliderX = (sliderPos / 100) * containerWidth;
|
||||||
const buffer = 40;
|
const buffer = 40;
|
||||||
const leftTrigger = margin + panelWidth + buffer;
|
const leftTrigger = margin + panelWidth + buffer;
|
||||||
@@ -88,21 +92,29 @@ $effect(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$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) {
|
if (containerWidth > 0 && panelWidth > 0) {
|
||||||
|
const targetX = currentSide === 'right'
|
||||||
|
? containerWidth - panelWidth - margin * 2
|
||||||
|
: 0;
|
||||||
|
|
||||||
// On side change set the position and the rotation
|
// On side change set the position and the rotation
|
||||||
xSpring.target = targetX;
|
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(() => {
|
timeoutId = setTimeout(() => {
|
||||||
rotateSpring.target = 0;
|
rotateSpring.target = 0;
|
||||||
}, 600);
|
}, 600);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timeoutId) {
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user