feat(ComparisonSlider): create a scrollable list of fonts with clever controls
This commit is contained in:
@@ -0,0 +1,202 @@
|
|||||||
|
<!--
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{#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 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(
|
||||||
|
'font-mono text-[10px] sm:text-[11px] uppercase 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 && 'text-indigo-600',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
--- {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>
|
||||||
Reference in New Issue
Block a user