216 lines
6.7 KiB
Svelte
216 lines
6.7 KiB
Svelte
<!--
|
|
Component: FontList
|
|
A scrollable list of fonts with dual selection buttons for fontA and fontB.
|
|
-->
|
|
<script lang="ts">
|
|
import {
|
|
FontVirtualList,
|
|
type UnifiedFont,
|
|
} from '$entities/Font';
|
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|
import { comparisonStore } from '$widgets/ComparisonSlider/model';
|
|
import { cubicOut } from 'svelte/easing';
|
|
import { draw } from 'svelte/transition';
|
|
|
|
const fontA = $derived(comparisonStore.fontA);
|
|
const fontB = $derived(comparisonStore.fontB);
|
|
const typography = $derived(comparisonStore.typography);
|
|
|
|
/**
|
|
* Select a font as fontA (right slot - compare_to)
|
|
*/
|
|
function selectFontA(font: UnifiedFont) {
|
|
comparisonStore.fontA = font;
|
|
}
|
|
|
|
/**
|
|
* Select a font as fontB (left slot - compare_from)
|
|
*/
|
|
function selectFontB(font: UnifiedFont) {
|
|
comparisonStore.fontB = font;
|
|
}
|
|
|
|
/**
|
|
* Check if a font is selected as fontA
|
|
*/
|
|
function isFontA(font: UnifiedFont): boolean {
|
|
return fontA?.id === font.id;
|
|
}
|
|
|
|
/**
|
|
* Check if a font is selected as fontB
|
|
*/
|
|
function isFontB(font: UnifiedFont): boolean {
|
|
return fontB?.id === font.id;
|
|
}
|
|
</script>
|
|
|
|
{#snippet rightBrackets(className?: string)}
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class={cn(
|
|
'lucide lucide-focus-icon lucide-focus right-0 top-0 absolute',
|
|
className,
|
|
)}
|
|
>
|
|
<path
|
|
transition:draw={{ duration: 300, delay: 150, easing: cubicOut }}
|
|
d="M17 3h2a2 2 0 0 1 2 2v2"
|
|
/>
|
|
</svg>
|
|
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class={cn(
|
|
'lucide lucide-focus-icon lucide-focus right-0 bottom-0 absolute',
|
|
className,
|
|
)}
|
|
>
|
|
<path
|
|
transition:draw={{ duration: 300, delay: 150, easing: cubicOut }}
|
|
d="M21 17v2a2 2 0 0 1-2 2h-2"
|
|
/>
|
|
</svg>
|
|
{/snippet}
|
|
|
|
{#snippet leftBrackets(className?: string)}
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class={cn(
|
|
'lucide lucide-focus-icon lucide-focus left-0 top-0 absolute',
|
|
className,
|
|
)}
|
|
>
|
|
<path
|
|
transition:draw={{ duration: 300, delay: 150, easing: cubicOut }}
|
|
d="M3 7V5a2 2 0 0 1 2-2h2"
|
|
/>
|
|
</svg>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class={cn(
|
|
'lucide lucide-focus-icon lucide-focus left-0 bottom-0 absolute',
|
|
className,
|
|
)}
|
|
>
|
|
<path
|
|
transition:draw={{ duration: 300, delay: 150, easing: cubicOut }}
|
|
d="M7 21H5a2 2 0 0 1-2-2v-2"
|
|
/>
|
|
</svg>
|
|
{/snippet}
|
|
|
|
{#snippet brackets(
|
|
renderLeft?: boolean,
|
|
renderRight?: boolean,
|
|
className?: string,
|
|
)}
|
|
{#if renderLeft}
|
|
{@render leftBrackets(className)}
|
|
{/if}
|
|
{#if renderRight}
|
|
{@render rightBrackets(className)}
|
|
{/if}
|
|
{/snippet}
|
|
|
|
<div class="flex flex-col h-full min-h-0 bg-transparent">
|
|
<div class="flex-1 min-h-0">
|
|
<FontVirtualList
|
|
weight={typography.weight}
|
|
itemHeight={36}
|
|
class="bg-transparent h-full"
|
|
>
|
|
{#snippet children({ item: font })}
|
|
{@const isSelectedA = isFontA(font)}
|
|
{@const isSelectedB = isFontB(font)}
|
|
{@const isEither = isSelectedA || isSelectedB}
|
|
{@const isBoth = isSelectedA && isSelectedB}
|
|
{@const handleSelectFontA = () => selectFontA(font)}
|
|
{@const handleSelectFontB = () => selectFontB(font)}
|
|
|
|
<div class="group relative flex w-auto h-[36px] border-b border-black/[0.03] overflow-hidden sm:mr-4 lg:mr-6">
|
|
<div
|
|
class={cn(
|
|
'absolute inset-0 flex items-center justify-center z-20 pointer-events-none transition-all duration-500 cubic-bezier-out',
|
|
isSelectedB && !isBoth && '-translate-x-1/4',
|
|
isSelectedA && !isBoth && 'translate-x-1/4',
|
|
isBoth && 'translate-x-0',
|
|
)}
|
|
>
|
|
<div class="relative flex items-center px-6">
|
|
<span
|
|
class={cn(
|
|
'text-[0.625rem] sm:text-[0.75rem] tracking-tighter select-none transition-all duration-300',
|
|
isEither
|
|
? 'opacity-100 font-bold'
|
|
: 'opacity-30 group-hover:opacity-100',
|
|
isSelectedB && 'text-indigo-500',
|
|
isSelectedA && 'text-normal-950',
|
|
isBoth
|
|
&& 'bg-[linear-gradient(to_right,theme(colors.indigo.500)_50%,theme(colors.neutral.950)_50%)] bg-clip-text text-transparent',
|
|
)}
|
|
>
|
|
--- {font.name} ---
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onclick={handleSelectFontB}
|
|
class="flex-1 relative flex items-center justify-between transition-all duration-200 cursor-pointer hover:bg-indigo-500/[0.03]"
|
|
>
|
|
{@render brackets(
|
|
isSelectedB,
|
|
isSelectedB && !isBoth,
|
|
'stroke-1 size-7 stroke-indigo-600',
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
onclick={handleSelectFontA}
|
|
class="flex-1 relative flex items-center justify-end transition-all duration-200 cursor-pointer hover:bg-black/[0.02]"
|
|
>
|
|
{@render brackets(
|
|
isSelectedA && !isBoth,
|
|
isSelectedA,
|
|
'stroke-1 size-7 stroke-normal-950',
|
|
)}
|
|
</button>
|
|
</div>
|
|
{/snippet}
|
|
</FontVirtualList>
|
|
</div>
|
|
</div>
|