From f01299f3d13d788c9af464a2908862ac74380ba8 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 10 Feb 2026 21:15:39 +0300 Subject: [PATCH 01/73] feat(smoothScroll): add util to smoothly scroll to the id after anchor click --- .../lib/utils/smoothScroll/smoothScroll.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/shared/lib/utils/smoothScroll/smoothScroll.ts diff --git a/src/shared/lib/utils/smoothScroll/smoothScroll.ts b/src/shared/lib/utils/smoothScroll/smoothScroll.ts new file mode 100644 index 0000000..9f3c616 --- /dev/null +++ b/src/shared/lib/utils/smoothScroll/smoothScroll.ts @@ -0,0 +1,32 @@ +/** + * Smoothly scrolls to the target element when an anchor element is clicked. + * @param node - The anchor element to listen for clicks on. + */ +export function smoothScroll(node: HTMLAnchorElement) { + const handleClick = (event: MouseEvent) => { + event.preventDefault(); + + const hash = node.getAttribute('href'); + if (!hash || hash === '#') return; + + const targetElement = document.querySelector(hash); + + if (targetElement) { + targetElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + + // Update URL hash without jumping + history.pushState(null, '', hash); + } + }; + + node.addEventListener('click', handleClick); + + return { + destroy() { + node.removeEventListener('click', handleClick); + }, + }; +} From a5b9238306c0ba8757bc8bcbd5daafb3152c1248 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 10 Feb 2026 21:15:52 +0300 Subject: [PATCH 02/73] chore: add export/import --- src/shared/lib/index.ts | 9 ++++++++- src/shared/lib/utils/index.ts | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index 99ade48..e2f6b3f 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -24,7 +24,14 @@ export { type VirtualizerOptions, } from './helpers'; -export { splitArray } from './utils'; +export { + buildQueryString, + clampNumber, + getDecimalPlaces, + roundToStepPrecision, + smoothScroll, + splitArray, +} from './utils'; export { springySlideFade } from './transitions'; diff --git a/src/shared/lib/utils/index.ts b/src/shared/lib/utils/index.ts index 08a788f..8bfc45f 100644 --- a/src/shared/lib/utils/index.ts +++ b/src/shared/lib/utils/index.ts @@ -11,4 +11,5 @@ export { clampNumber } from './clampNumber/clampNumber'; export { debounce } from './debounce/debounce'; export { getDecimalPlaces } from './getDecimalPlaces/getDecimalPlaces'; export { roundToStepPrecision } from './roundToStepPrecision/roundToStepPrecision'; +export { smoothScroll } from './smoothScroll/smoothScroll'; export { splitArray } from './splitArray/splitArray'; From a557e157592e300cc9442814790ecfba6ced1139 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 10 Feb 2026 21:16:32 +0300 Subject: [PATCH 03/73] feat(scrollBreadcrumbStore): add id field and comments --- .../model/store/scrollBreadcrumbsStore.svelte.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts b/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts index f914c5e..35f474f 100644 --- a/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts +++ b/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts @@ -1,7 +1,17 @@ import type { Snippet } from 'svelte'; export interface BreadcrumbItem { + /** + * Index of the item to display + */ index: number; + /** + * ID of the item to navigate to + */ + id?: string; + /** + * Title snippet to render + */ title: Snippet<[{ className?: string }]>; } From 2508168a3ebc3842fc0a2749b8f80560a772786a Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 10 Feb 2026 21:17:50 +0300 Subject: [PATCH 04/73] feat(Section): add id prop and pass it to onTitleStatusChange callback --- src/shared/ui/Section/Section.svelte | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/shared/ui/Section/Section.svelte b/src/shared/ui/Section/Section.svelte index f8fa12c..9412495 100644 --- a/src/shared/ui/Section/Section.svelte +++ b/src/shared/ui/Section/Section.svelte @@ -14,6 +14,10 @@ import { import { Footnote } from '..'; interface Props extends Omit, 'title'> { + /** + * ID of the section + */ + id?: string; /** * Additional CSS classes to apply to the section container. */ @@ -40,16 +44,22 @@ interface Props extends Omit, 'title'> { * @param index - Index of the section * @param isPast - Whether the section is past the current scroll position * @param title - Snippet for a title itself + * @param id - ID of the section * @returns Cleanup callback */ - onTitleStatusChange?: (index: number, isPast: boolean, title?: Snippet<[{ className?: string }]>) => () => void; + onTitleStatusChange?: ( + index: number, + isPast: boolean, + title?: Snippet<[{ className?: string }]>, + id?: string, + ) => () => void; /** * Snippet for the section content */ children?: Snippet; } -const { class: className, title, icon, description, index = 0, onTitleStatusChange, children }: Props = $props(); +const { class: className, title, icon, description, index = 0, onTitleStatusChange, id, children }: Props = $props(); let titleContainer = $state(); const flyParams: FlyParams = { y: 0, x: -50, duration: 300, easing: cubicOut, opacity: 0.2 }; @@ -68,7 +78,7 @@ $effect(() => { if (isPast !== isScrolledPast) { isScrolledPast = isPast; - cleanup = onTitleStatusChange?.(index, isPast, title); + cleanup = onTitleStatusChange?.(index, isPast, title, id); } }, { // Set threshold to 0 to trigger exactly when the last pixel leaves @@ -84,6 +94,7 @@ $effect(() => {
Date: Tue, 10 Feb 2026 21:18:49 +0300 Subject: [PATCH 05/73] feat(Page): add id and pass it to scrollBreadcrumbStore --- src/routes/Page.svelte | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/routes/Page.svelte b/src/routes/Page.svelte index bd5f546..d6b6bcb 100644 --- a/src/routes/Page.svelte +++ b/src/routes/Page.svelte @@ -23,9 +23,14 @@ let searchContainer: HTMLElement; let isExpanded = $state(false); -function handleTitleStatusChanged(index: number, isPast: boolean, title?: Snippet<[{ className?: string }]>) { +function handleTitleStatusChanged( + index: number, + isPast: boolean, + title?: Snippet<[{ className?: string }]>, + id?: string, +) { if (isPast && title) { - scrollBreadcrumbsStore.add({ index, title }); + scrollBreadcrumbsStore.add({ index, title, id }); } else { scrollBreadcrumbsStore.remove(index); } @@ -50,7 +55,7 @@ function handleTitleStatusChanged(index: number, isPast: boolean, title?: Snippe class="p-2 sm:p-3 md:p-4 h-full flex flex-col gap-3 sm:gap-4" in:fade={{ duration: 500, delay: 150, easing: cubicIn }} > -
+
{#snippet icon({ className })} {/snippet} @@ -62,7 +67,12 @@ function handleTitleStatusChanged(index: number, isPast: boolean, title?: Snippe
-
+
{#snippet icon({ className })} {/snippet} @@ -74,7 +84,12 @@ function handleTitleStatusChanged(index: number, isPast: boolean, title?: Snippe
-
+
{#snippet icon({ className })} {/snippet} @@ -86,7 +101,12 @@ function handleTitleStatusChanged(index: number, isPast: boolean, title?: Snippe
-
+
{#snippet icon({ className })} {/snippet} From 8aad8942fcb3eafe45e6f7c4c2f666e3b2160207 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 10 Feb 2026 21:19:30 +0300 Subject: [PATCH 06/73] feat(BreadcrumbHeader): add anchor to scroll to the section from the breadcrumb --- .../Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.svelte | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.svelte b/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.svelte index 1398d23..84352e4 100644 --- a/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.svelte +++ b/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.svelte @@ -3,6 +3,7 @@ Fixed header for breadcrumbs navigation for sections in the page --> diff --git a/src/shared/ui/IconButton/IconButton.svelte b/src/shared/ui/IconButton/IconButton.svelte index 6f7a6e2..0649394 100644 --- a/src/shared/ui/IconButton/IconButton.svelte +++ b/src/shared/ui/IconButton/IconButton.svelte @@ -29,7 +29,7 @@ let { rotation = 'clockwise', icon, ...rest }: Props = $props(); variant="ghost" class=" group relative border-none size-9 - bg-white/20 hover:bg-white/60 + bg-background-20 hover:bg-background-60 backdrop-blur-3xl transition-all duration-200 ease-out will-change-transform @@ -43,7 +43,7 @@ let { rotation = 'clockwise', icon, ...rest }: Props = $props(); > {@render icon({ className: cn( - 'size-4 transition-all duration-200 stroke-[1.5] stroke-gray-500 group-hover:stroke-gray-900 group-hover:scale-110 group-hover:stroke-3 group-active:scale-90 group-disabled:stroke-transparent', + 'size-4 transition-all duration-200 stroke-[1.5] stroke-text-muted group-hover:stroke-foreground group-hover:scale-110 group-hover:stroke-3 group-active:scale-90 group-disabled:stroke-transparent', rotation === 'clockwise' ? 'group-active:rotate-6' : 'group-active:-rotate-6', ), })} diff --git a/src/shared/ui/Input/Input.svelte b/src/shared/ui/Input/Input.svelte index da1e393..73e7a50 100644 --- a/src/shared/ui/Input/Input.svelte +++ b/src/shared/ui/Input/Input.svelte @@ -35,19 +35,19 @@ const isGhost = $derived(variant === 'ghost'); class={cn( 'h-12 sm:h-14 md:h-16 w-full text-sm sm:text-base', 'backdrop-blur-md', - isGhost ? 'bg-transparent' : 'bg-white/80', - 'border border-gray-300/50', - isGhost ? 'border-transparent' : 'border-gray-300/50', + isGhost ? 'bg-transparent' : 'bg-background-80', + 'border border-border-muted', + isGhost ? 'border-transparent' : 'border-border-muted', isGhost ? 'shadow-none' : 'shadow-[0_1px_3px_rgba(0,0,0,0.04)]', - 'focus-visible:border-gray-400/60', + 'focus-visible:border-border-soft', 'focus-visible:outline-none', 'focus-visible:ring-1', - 'focus-visible:ring-gray-400/30', - 'focus-visible:bg-white/90', - 'hover:bg-white/90', - 'hover:border-gray-400/60', - 'text-gray-900', - 'placeholder:text-gray-400', + 'focus-visible:ring-border-muted/30', + 'focus-visible:bg-background-95', + 'hover:bg-background-95', + 'hover:border-border-soft', + 'text-foreground', + 'placeholder:text-text-muted', 'placeholder:font-mono', 'placeholder:text-xs sm:placeholder:text-sm', 'placeholder:tracking-wide', diff --git a/src/shared/ui/Loader/Loader.svelte b/src/shared/ui/Loader/Loader.svelte index 2789f74..2b976d6 100644 --- a/src/shared/ui/Loader/Loader.svelte +++ b/src/shared/ui/Loader/Loader.svelte @@ -31,7 +31,7 @@ let { size = 20, class: className = '', message = 'analyzing_data' }: Props = $p out:fade={{ duration: 300 }} >
- + @@ -68,10 +68,10 @@ let { size = 20, class: className = '', message = 'analyzing_data' }: Props = $p
-
+
- + {message} diff --git a/src/shared/ui/Section/Section.svelte b/src/shared/ui/Section/Section.svelte index 9412495..6e63940 100644 --- a/src/shared/ui/Section/Section.svelte +++ b/src/shared/ui/Section/Section.svelte @@ -105,8 +105,8 @@ $effect(() => {
{#if icon} - {@render icon({ className: 'size-3 sm:size-4 stroke-gray-900 stroke-1 opacity-60' })} -
+ {@render icon({ className: 'size-3 sm:size-4 stroke-foreground stroke-1 opacity-60' })} +
{/if} {#if description} @@ -124,7 +124,7 @@ $effect(() => { {#if title} {@render title({ className: - 'text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-semibold tracking-tighter text-gray-900 leading-[0.9]', + 'text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-semibold tracking-tighter text-foreground leading-[0.9]', })} {/if}
diff --git a/src/shared/ui/Skeleton/Skeleton.stories.svelte b/src/shared/ui/Skeleton/Skeleton.stories.svelte index 426efb8..24c3635 100644 --- a/src/shared/ui/Skeleton/Skeleton.stories.svelte +++ b/src/shared/ui/Skeleton/Skeleton.stories.svelte @@ -30,7 +30,7 @@ const { Story } = defineMeta({ }} >
-
+
diff --git a/src/shared/ui/Skeleton/Skeleton.svelte b/src/shared/ui/Skeleton/Skeleton.svelte index 0e28399..0cf9bd1 100644 --- a/src/shared/ui/Skeleton/Skeleton.svelte +++ b/src/shared/ui/Skeleton/Skeleton.svelte @@ -18,7 +18,7 @@ let { class: className, animate = true, ...rest }: Props = $props();
{ rounded-xl sm:rounded-2xl md:rounded-[2.5rem] select-none touch-none cursor-ew-resize min-h-72 sm:min-h-96 flex flex-col justify-center backdrop-blur-lg bg-linear-to-br from-gray-100/70 via-white/50 to-gray-100/60 - border border-gray-300/40 + border border-border-muted shadow-[inset_0_4px_12px_0_rgba(0,0,0,0.12),inset_0_2px_4px_0_rgba(0,0,0,0.08),0_1px_2px_0_rgba(255,255,255,0.8)] before:absolute before:inset-0 before:rounded-xl sm:before:rounded-2xl md:before:rounded-[2.5rem] before:p-px before:bg-linear-to-br before:from-black/5 before:via-black/2 before:to-transparent diff --git a/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SelectComparedFonts.svelte b/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SelectComparedFonts.svelte index 379776b..45c494a 100644 --- a/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SelectComparedFonts.svelte +++ b/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SelectComparedFonts.svelte @@ -63,10 +63,10 @@ function selectFontB(font: UnifiedFont) {
@@ -77,7 +77,7 @@ function selectFontB(font: UnifiedFont) { onSelect(fontListItem)}
-
- +
+ ch_01
@@ -133,11 +133,11 @@ function selectFontB(font: UnifiedFont) { style:transform="translateY({sliderPos > 80 ? '8px' : '0px'})" >
- + ch_02 -
-
+
+
{#if fontA && fontAUrl} {@render fontSelector(fontA, fontList, fontAUrl, selectFontA, 'end')} diff --git a/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SliderLine.svelte b/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SliderLine.svelte index 53805c1..f05a53e 100644 --- a/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SliderLine.svelte +++ b/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SliderLine.svelte @@ -51,7 +51,7 @@ let { sliderPos, isDragging }: Props = $props();
-
+
{#snippet icon({ className })} {/snippet} @@ -102,14 +102,14 @@ function toggleFilters() {
-
-
+
+
filter_params @@ -119,7 +119,7 @@ function toggleFilters() {
-
+
From 173816b5c097f778afcb8db8d496319215f21f13 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 10:29:08 +0300 Subject: [PATCH 08/73] feat(lenis): add smooth scroll solution --- .../createScrollContext.svelte.ts | 32 ++++++++ src/shared/lib/helpers/index.ts | 6 ++ .../ui/SmoothScroll/SmoothScroll.svelte | 78 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/shared/lib/helpers/createScrollContext/createScrollContext.svelte.ts create mode 100644 src/shared/ui/SmoothScroll/SmoothScroll.svelte diff --git a/src/shared/lib/helpers/createScrollContext/createScrollContext.svelte.ts b/src/shared/lib/helpers/createScrollContext/createScrollContext.svelte.ts new file mode 100644 index 0000000..87703d2 --- /dev/null +++ b/src/shared/lib/helpers/createScrollContext/createScrollContext.svelte.ts @@ -0,0 +1,32 @@ +import Lenis from 'lenis'; +import { + getContext, + setContext, +} from 'svelte'; + +const LENIS_KEY = Symbol('lenis'); + +export function createLenisContext() { + let lenis = $state(null); + + return { + get lenis() { + return lenis; + }, + setLenis(instance: Lenis) { + lenis = instance; + }, + destroyLenis() { + lenis?.destroy(); + lenis = null; + }, + }; +} + +export function setLenisContext(context: ReturnType) { + setContext(LENIS_KEY, context); +} + +export function getLenisContext() { + return getContext>(LENIS_KEY); +} diff --git a/src/shared/lib/helpers/index.ts b/src/shared/lib/helpers/index.ts index 41139b0..63890d1 100644 --- a/src/shared/lib/helpers/index.ts +++ b/src/shared/lib/helpers/index.ts @@ -42,3 +42,9 @@ export { type ResponsiveManager, responsiveManager, } from './createResponsiveManager/createResponsiveManager.svelte'; + +export { + createLenisContext, + getLenisContext, + setLenisContext, +} from './createScrollContext/createScrollContext.svelte'; diff --git a/src/shared/ui/SmoothScroll/SmoothScroll.svelte b/src/shared/ui/SmoothScroll/SmoothScroll.svelte new file mode 100644 index 0000000..6fe199e --- /dev/null +++ b/src/shared/ui/SmoothScroll/SmoothScroll.svelte @@ -0,0 +1,78 @@ + + +{@render children?.()} From 2e6fc0e858d8352c425774236e8f87dbed460b04 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 10:29:52 +0300 Subject: [PATCH 09/73] feat(throttle): add tohrottling util --- src/shared/lib/utils/throttle/throttle.ts | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/shared/lib/utils/throttle/throttle.ts diff --git a/src/shared/lib/utils/throttle/throttle.ts b/src/shared/lib/utils/throttle/throttle.ts new file mode 100644 index 0000000..4bb0c90 --- /dev/null +++ b/src/shared/lib/utils/throttle/throttle.ts @@ -0,0 +1,32 @@ +/** + * Throttle function execution to a maximum frequency. + * + * @param fn Function to throttle. + * @param wait Maximum time between function calls. + * @returns Throttled function. + */ +export function throttle any>( + fn: T, + wait: number, +): (...args: Parameters) => void { + let lastCall = 0; + let timeoutId: ReturnType | null = null; + + return (...args: Parameters) => { + const now = Date.now(); + const timeSinceLastCall = now - lastCall; + + if (timeSinceLastCall >= wait) { + lastCall = now; + fn(...args); + } else { + // Schedule for end of wait period + if (timeoutId) clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + lastCall = Date.now(); + fn(...args); + timeoutId = null; + }, wait - timeSinceLastCall); + } + }; +} From 08d474289b586c40ab065c7e8882787938d5efb0 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 10:30:43 +0300 Subject: [PATCH 10/73] chore: add export/import --- src/shared/lib/index.ts | 5 +++++ src/shared/lib/utils/index.ts | 1 + src/shared/ui/index.ts | 1 + 3 files changed, 7 insertions(+) diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index e2f6b3f..4effc6f 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -5,6 +5,7 @@ export { createDebouncedState, createEntityStore, createFilter, + createLenisContext, createPersistentStore, createResponsiveManager, createTypographyControl, @@ -13,11 +14,13 @@ export { type EntityStore, type Filter, type FilterModel, + getLenisContext, type LineData, type PersistentStore, type Property, type ResponsiveManager, responsiveManager, + setLenisContext, type TypographyControl, type VirtualItem, type Virtualizer, @@ -27,10 +30,12 @@ export { export { buildQueryString, clampNumber, + debounce, getDecimalPlaces, roundToStepPrecision, smoothScroll, splitArray, + throttle, } from './utils'; export { springySlideFade } from './transitions'; diff --git a/src/shared/lib/utils/index.ts b/src/shared/lib/utils/index.ts index 8bfc45f..904d58c 100644 --- a/src/shared/lib/utils/index.ts +++ b/src/shared/lib/utils/index.ts @@ -13,3 +13,4 @@ export { getDecimalPlaces } from './getDecimalPlaces/getDecimalPlaces'; export { roundToStepPrecision } from './roundToStepPrecision/roundToStepPrecision'; export { smoothScroll } from './smoothScroll/smoothScroll'; export { splitArray } from './splitArray/splitArray'; +export { throttle } from './throttle/throttle'; diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 0adbb94..d1e57cf 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -13,4 +13,5 @@ export { default as SearchBar } from './SearchBar/SearchBar.svelte'; export { default as Section } from './Section/Section.svelte'; export { default as Skeleton } from './Skeleton/Skeleton.svelte'; export { default as Slider } from './Slider/Slider.svelte'; +export { default as SmoothScroll } from './SmoothScroll/SmoothScroll.svelte'; export { default as VirtualList } from './VirtualList/VirtualList.svelte'; From 3423eebf77b3b810f4e64853940a67bb978b4d3c Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 10:31:02 +0300 Subject: [PATCH 11/73] feat: install lenis --- package.json | 3 ++- yarn.lock | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c98f54b..95903f8 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "vitest-browser-svelte": "^2.0.1" }, "dependencies": { - "@tanstack/svelte-query": "^6.0.14" + "@tanstack/svelte-query": "^6.0.14", + "lenis": "^1.3.17" } } diff --git a/yarn.lock b/yarn.lock index 84894cd..5b0112b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2459,6 +2459,7 @@ __metadata: dprint: "npm:^0.50.2" jsdom: "npm:^27.4.0" lefthook: "npm:^2.0.13" + lenis: "npm:^1.3.17" oxlint: "npm:^1.35.0" playwright: "npm:^1.57.0" storybook: "npm:^10.1.11" @@ -2849,6 +2850,24 @@ __metadata: languageName: node linkType: hard +"lenis@npm:^1.3.17": + version: 1.3.17 + resolution: "lenis@npm:1.3.17" + peerDependencies: + "@nuxt/kit": ">=3.0.0" + react: ">=17.0.0" + vue: ">=3.0.0" + peerDependenciesMeta: + "@nuxt/kit": + optional: true + react: + optional: true + vue: + optional: true + checksum: 10c0/c268da36d5711677b239c7d173bc52775276df08f86f7f89f305c4e02ba4055d8c50ea69125d16c94bb1e1999ccd95f654237d11c6647dc5fdf63aa90515fbfb + languageName: node + linkType: hard + "lightningcss-android-arm64@npm:1.30.2": version: 1.30.2 resolution: "lightningcss-android-arm64@npm:1.30.2" From cdb2c355c02808200d895291e7905e279ab9eb13 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 10:31:23 +0300 Subject: [PATCH 12/73] fix: add types for env variables --- src/app/types/ambient.d.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/app/types/ambient.d.ts b/src/app/types/ambient.d.ts index d2f5b6e..4700e35 100644 --- a/src/app/types/ambient.d.ts +++ b/src/app/types/ambient.d.ts @@ -35,3 +35,16 @@ declare module '*.jpg' { const content: string; export default content; } + +/// + +interface ImportMetaEnv { + readonly DEV: boolean; + readonly PROD: boolean; + readonly MODE: string; + // Add other env variables you use +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} From 21d8273967d3cafa1030429d1daadf01868267c4 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 10:32:25 +0300 Subject: [PATCH 13/73] feat(VirtualList): add throttling --- src/shared/ui/VirtualList/VirtualList.svelte | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/shared/ui/VirtualList/VirtualList.svelte b/src/shared/ui/VirtualList/VirtualList.svelte index ff833b4..71d066a 100644 --- a/src/shared/ui/VirtualList/VirtualList.svelte +++ b/src/shared/ui/VirtualList/VirtualList.svelte @@ -10,6 +10,7 @@ --> + {/snippet} From 5e3929575d764168954f5b095d76f9561051046e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 11:14:22 +0300 Subject: [PATCH 15/73] feat(FontApplicator): remove IntersectionObserver to ease the product, font applying logic is entirely in the VirtualList --- .../ui/FontApplicator/FontApplicator.svelte | 65 +++++++------------ 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/src/entities/Font/ui/FontApplicator/FontApplicator.svelte b/src/entities/Font/ui/FontApplicator/FontApplicator.svelte index a6076ec..4126522 100644 --- a/src/entities/Font/ui/FontApplicator/FontApplicator.svelte +++ b/src/entities/Font/ui/FontApplicator/FontApplicator.svelte @@ -2,11 +2,10 @@ Component: FontApplicator Loads fonts from fontshare with link tag - Loads font only if it's not already applied - - Uses IntersectionObserver to detect when font is visible + - Reacts to font load status to show/hide content - Adds smooth transition when font appears -->
Date: Thu, 12 Feb 2026 11:16:01 +0300 Subject: [PATCH 16/73] feat(FontSampler): remove backdrop filter since it's not being used and bad for performance --- src/features/DisplayFont/ui/FontSampler/FontSampler.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte index 8c4c520..a2ede04 100644 --- a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte +++ b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte @@ -53,7 +53,7 @@ const letterSpacing = $derived(controlManager.spacing); class=" w-full h-full rounded-xl sm:rounded-2xl flex flex-col - backdrop-blur-md bg-background-80 + bg-background-80 border border-border-muted shadow-[0_1px_3px_rgba(0,0,0,0.04)] relative overflow-hidden From 0e85851cfd76bab43ed860b28a6bdeac8ee99ecc Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 11:21:04 +0300 Subject: [PATCH 17/73] fix(FontApplicator): remove unused prop --- src/entities/Font/ui/FontApplicator/FontApplicator.svelte | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/entities/Font/ui/FontApplicator/FontApplicator.svelte b/src/entities/Font/ui/FontApplicator/FontApplicator.svelte index 4126522..7c0fb0b 100644 --- a/src/entities/Font/ui/FontApplicator/FontApplicator.svelte +++ b/src/entities/Font/ui/FontApplicator/FontApplicator.svelte @@ -49,10 +49,7 @@ const status = $derived( ); // The "Show" condition: Font is loaded OR it errored out OR it's a noTouch preview (like in search) -const shouldReveal = $derived.by(() => { - if (noTouch) return true; - return status === 'loaded' || status === 'error'; -}); +const shouldReveal = $derived(status === 'loaded' || status === 'error'); const transitionClasses = $derived( prefersReducedMotion.current From 8b02333c01ba6f936ab7085cb23fdd4507a5e552 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 11:23:27 +0300 Subject: [PATCH 18/73] feat(createVirtualizer): slidthly improve batching with version trigger --- .../createVirtualizer.svelte.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts index 4b92eb3..9fabb92 100644 --- a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts +++ b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts @@ -120,9 +120,11 @@ export function createVirtualizer( // By wrapping the getter in $derived, we track everything inside it const options = $derived(optionsGetter()); - // This derivation now tracks: count, measuredSizes, AND the data array itself + // This derivation now tracks: count, _version (for measuredSizes updates), AND the data array itself const offsets = $derived.by(() => { const count = options.count; + // Implicit dependency on version signal + const v = _version; const result = new Float64Array(count); let accumulated = 0; for (let i = 0; i < count; i++) { @@ -144,6 +146,8 @@ export function createVirtualizer( // We MUST read options.data here so Svelte knows to re-run // this derivation when the items array is replaced! const { count, data } = options; + // Implicit dependency + const v = _version; if (count === 0 || containerHeight === 0 || !data) return []; const overscan = options.overscan ?? 5; @@ -318,6 +322,9 @@ export function createVirtualizer( let measurementBuffer: Record = {}; let frameId: number | null = null; + // Signal to trigger updates when mutating measuredSizes in place + let _version = $state(0); + /** * Svelte action to measure individual item elements for dynamic height support. * @@ -334,18 +341,25 @@ export function createVirtualizer( const height = entry.borderBoxSize[0]?.blockSize ?? node.offsetHeight; if (!isNaN(index)) { + // Accessing the version ensures we have the latest state if needed, + // though here we just read the raw object. const oldHeight = measuredSizes[index]; + // Only update if the height difference is significant (> 0.5px) - // This prevents "jitter" from focus rings or sub-pixel border changes if (oldHeight === undefined || Math.abs(oldHeight - height) > 0.5) { - // Stuff the measurement into a temporary buffer + // Stuff the measurement into a temporary buffer to batch updates measurementBuffer[index] = height; // Schedule a single update for the next animation frame if (frameId === null) { frameId = requestAnimationFrame(() => { - measuredSizes = { ...measuredSizes, ...measurementBuffer }; - // Reset the buffer + // Mutation in place for performance + Object.assign(measuredSizes, measurementBuffer); + + // Trigger reactivity + _version += 1; + + // Reset buffer measurementBuffer = {}; frameId = null; }); From cee2a80c4106376cdd05cbdf077ae496addf547a Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 11:24:16 +0300 Subject: [PATCH 19/73] feat(FontListItem): delete springs to imrove performance --- .../Font/ui/FontListItem/FontListItem.svelte | 52 ++----------------- 1 file changed, 5 insertions(+), 47 deletions(-) diff --git a/src/entities/Font/ui/FontListItem/FontListItem.svelte b/src/entities/Font/ui/FontListItem/FontListItem.svelte index b958d69..9ea82c1 100644 --- a/src/entities/Font/ui/FontListItem/FontListItem.svelte +++ b/src/entities/Font/ui/FontListItem/FontListItem.svelte @@ -1,11 +1,6 @@ -
{@render children?.(font)}
From f2e8de1d1dd3831c5018468d15acbec36756d6eb Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 12:19:11 +0300 Subject: [PATCH 20/73] feat(comparisonStore): add the check before loading --- .../model/stores/comparisonStore.svelte.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/widgets/ComparisonSlider/model/stores/comparisonStore.svelte.ts b/src/widgets/ComparisonSlider/model/stores/comparisonStore.svelte.ts index d277e5f..270bb96 100644 --- a/src/widgets/ComparisonSlider/model/stores/comparisonStore.svelte.ts +++ b/src/widgets/ComparisonSlider/model/stores/comparisonStore.svelte.ts @@ -78,8 +78,6 @@ class ComparisonStore { return; } - this.#fontsReady = false; - const weight = this.#typography.weight; const size = this.#typography.renderedSize; const fontAName = this.#fontA?.name; @@ -87,11 +85,25 @@ class ComparisonStore { if (!fontAName || !fontBName) return; + const fontAString = `${weight} ${size}px "${fontAName}"`; + const fontBString = `${weight} ${size}px "${fontBName}"`; + + // Check if already loaded to avoid UI flash + const isALoaded = document.fonts.check(fontAString); + const isBLoaded = document.fonts.check(fontBString); + + if (isALoaded && isBLoaded) { + this.#fontsReady = true; + return; + } + + this.#fontsReady = false; + try { // Step 1: Load fonts into memory await Promise.all([ - document.fonts.load(`${weight} ${size}px "${fontAName}"`), - document.fonts.load(`${weight} ${size}px "${fontBName}"`), + document.fonts.load(fontAString), + document.fonts.load(fontBString), ]); // Step 2: Wait for browser to be ready to render From d282448c532b6a81eb927b88e874b3124947417f Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 12 Feb 2026 12:20:06 +0300 Subject: [PATCH 21/73] feat(CharacterSlot): remove touch from characters --- .../components/CharacterSlot.svelte | 54 ++++++------------- 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/CharacterSlot.svelte b/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/CharacterSlot.svelte index 3f174f1..3c6ce24 100644 --- a/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/CharacterSlot.svelte +++ b/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/CharacterSlot.svelte @@ -3,8 +3,6 @@ Renders a character with particular styling based on proximity, isPast, weight, fontAName, and fontBName. --> {#if fontA && fontB} @@ -67,13 +38,18 @@ $effect(() => { style:font-weight={typography.weight} style:font-size={`${typography.renderedSize}px`} style:transform=" - scale({1 + proximity * 0.3}) - translateY({-proximity * 12}px) - rotateY({proximity * 25 * (isPast ? -1 : 1)}deg) - " - style:filter="brightness({1 + proximity * 0.2}) contrast({1 + proximity * 0.1})" - style:text-shadow={proximity > 0.5 ? '0 0 15px rgba(99,102,241,0.3)' : 'none'} - style:will-change={proximity > 0 ? 'transform, font-family, color' : 'auto'} + scale({1 + proximity * 0.3}) translateY({-proximity * 12}px) rotateY({proximity * + 25 * + (isPast ? -1 : 1)}deg) + " + style:filter="brightness({1 + proximity * 0.2}) contrast({1 + + proximity * 0.1})" + style:text-shadow={proximity > 0.5 + ? '0 0 15px rgba(99,102,241,0.3)' + : 'none'} + style:will-change={proximity > 0 + ? 'transform, font-family, color' + : 'auto'} > {char === ' ' ? '\u00A0' : char} @@ -82,9 +58,9 @@ $effect(() => {