feature/ux-improvements #26

Merged
ilia merged 73 commits from feature/ux-improvements into main 2026-02-18 14:43:05 +00:00
Showing only changes of commit 858daff860 - Show all commits

View File

@@ -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>