From b5ad3249ae133cc612dae953ee0ec2d5f3888728 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 20 Jan 2026 09:32:12 +0300 Subject: [PATCH 001/129] feat(ComparisonSlider): create reusable comparison slider that compare two fonts for the same text. Line breaking is supported --- .../createCharacterComparison.svelte.ts | 170 ++++++++++++++++ src/shared/lib/helpers/index.ts | 4 + src/shared/lib/index.ts | 1 + .../ComparisonSlider/ComparisonSlider.svelte | 182 ++++++++++++++++++ .../ComparisonSlider/components/Labels.svelte | 37 ++++ .../components/SliderLine.svelte | 26 +++ src/shared/ui/index.ts | 2 + 7 files changed, 422 insertions(+) create mode 100644 src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts create mode 100644 src/shared/ui/ComparisonSlider/ComparisonSlider.svelte create mode 100644 src/shared/ui/ComparisonSlider/components/Labels.svelte create mode 100644 src/shared/ui/ComparisonSlider/components/SliderLine.svelte diff --git a/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts b/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts new file mode 100644 index 0000000..6c2f458 --- /dev/null +++ b/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts @@ -0,0 +1,170 @@ +/** + * Interface representing a line of text with its measured width. + */ +export interface LineData { + text: string; + width: number; +} + +/** + * Creates a helper for splitting text into lines and calculating character proximity. + * This is used by the ComparisonSlider (TestTen) to render morphing text. + * + * @param text - The text to split and measure + * @param fontA - The first font definition + * @param fontB - The second font definition + * @returns Object with reactive state (lines, containerWidth) and methods (breakIntoLines, getCharState) + */ +export function createCharacterComparison( + text: () => string, + fontA: () => { name: string; id: string }, + fontB: () => { name: string; id: string }, +) { + let lines = $state([]); + let containerWidth = $state(0); + + /** + * Measures text width using a canvas context. + * @param ctx - Canvas rendering context + * @param text - Text string to measure + * @param fontFamily - Font family name + * @param fontSize - Font size in pixels + */ + function measureText( + ctx: CanvasRenderingContext2D, + text: string, + fontFamily: string, + fontSize: number, + ): number { + ctx.font = `bold ${fontSize}px ${fontFamily}`; + return ctx.measureText(text).width; + } + + /** + * Determines the appropriate font size based on window width. + * Matches the Tailwind breakpoints used in the component. + */ + function getFontSize() { + if (typeof window === 'undefined') return 64; + return window.innerWidth >= 1024 + ? 112 + : window.innerWidth >= 768 + ? 96 + : window.innerWidth >= 640 + ? 80 + : 64; + } + + /** + * Breaks the text into lines based on the container width and measure canvas. + * Populates the `lines` state. + * + * @param container - The container element to measure width from + * @param measureCanvas - The canvas element used for text measurement + */ + function breakIntoLines( + container: HTMLElement | undefined, + measureCanvas: HTMLCanvasElement | undefined, + ) { + if (!container || !measureCanvas) return; + + const rect = container.getBoundingClientRect(); + containerWidth = rect.width; + + // Padding considerations - matches the container padding + const padding = window.innerWidth < 640 ? 48 : 96; + const availableWidth = rect.width - padding; + + const ctx = measureCanvas.getContext('2d'); + if (!ctx) return; + + const fontSize = getFontSize(); + const words = text().split(' '); + const newLines: LineData[] = []; + let currentLineWords: string[] = []; + + function pushLine(words: string[]) { + if (words.length === 0) return; + const lineText = words.join(' '); + // Measure width to ensure we know exactly how wide this line renders + const widthA = measureText(ctx!, lineText, fontA().name, fontSize); + const widthB = measureText(ctx!, lineText, fontB().name, fontSize); + const maxWidth = Math.max(widthA, widthB); + newLines.push({ text: lineText, width: maxWidth }); + } + + for (const word of words) { + const testLine = currentLineWords.length > 0 + ? currentLineWords.join(' ') + ' ' + word + : word; + + // Measure with both fonts and use the wider one to prevent layout shifts + const widthA = measureText(ctx, testLine, fontA().name, fontSize); + const widthB = measureText(ctx, testLine, fontB().name, fontSize); + const maxWidth = Math.max(widthA, widthB); + + if (maxWidth > availableWidth && currentLineWords.length > 0) { + pushLine(currentLineWords); + currentLineWords = [word]; + } else { + currentLineWords.push(word); + } + } + + if (currentLineWords.length > 0) { + pushLine(currentLineWords); + } + + lines = newLines; + } + + /** + * precise calculation of character state based on global slider position. + * + * @param lineIndex - Index of the line + * @param charIndex - Index of the character in the line + * @param lineData - The line data object + * @param sliderPos - Current slider position (0-100) + * @returns Object containing proximity (0-1) and isPast (boolean) + */ + function getCharState( + lineIndex: number, + charIndex: number, + lineData: LineData, + sliderPos: number, + ) { + if (!containerWidth) return { proximity: 0, isPast: false }; + + // Calculate the pixel position of the character relative to the CONTAINER + // 1. Find the left edge of the centered line + const lineStartOffset = (containerWidth - lineData.width) / 2; + + // 2. Find the character's center relative to the line + const charRelativePercent = (charIndex + 0.5) / lineData.text.length; + const charPixelPos = lineStartOffset + (charRelativePercent * lineData.width); + + // 3. Convert back to global percentage (0-100) + const charGlobalPercent = (charPixelPos / containerWidth) * 100; + + const distance = Math.abs(sliderPos - charGlobalPercent); + + // Proximity range: +/- 15% around the slider + const range = 15; + const proximity = Math.max(0, 1 - distance / range); + + const isPast = sliderPos > charGlobalPercent; + + return { proximity, isPast }; + } + + return { + get lines() { + return lines; + }, + get containerWidth() { + return containerWidth; + }, + breakIntoLines, + getCharState, + }; +} diff --git a/src/shared/lib/helpers/index.ts b/src/shared/lib/helpers/index.ts index 62db226..61b065f 100644 --- a/src/shared/lib/helpers/index.ts +++ b/src/shared/lib/helpers/index.ts @@ -26,3 +26,7 @@ export { type Entity, type EntityStore, } from './createEntityStore/createEntityStore.svelte'; + +export { + createCharacterComparison, +} from './createCharacterComparison/createCharacterComparison.svelte'; diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index fea0978..9734dd5 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -1,6 +1,7 @@ export { type ControlDataModel, type ControlModel, + createCharacterComparison, createDebouncedState, createEntityStore, createFilter, diff --git a/src/shared/ui/ComparisonSlider/ComparisonSlider.svelte b/src/shared/ui/ComparisonSlider/ComparisonSlider.svelte new file mode 100644 index 0000000..00d493b --- /dev/null +++ b/src/shared/ui/ComparisonSlider/ComparisonSlider.svelte @@ -0,0 +1,182 @@ + + + + + + +
+ +
+
+ + +
+ {#each charComparison.lines as line, lineIndex} +
+ {#each line.text.split('') as char, charIndex} + {@const { proximity, isPast } = charComparison.getCharState(lineIndex, charIndex, line, sliderPos)} + + 0 + ? 'transform, font-family, color' + : 'auto'} + > + {char === ' ' ? '\u00A0' : char} + + {/each} +
+ {/each} +
+ + + + +
+ + diff --git a/src/shared/ui/ComparisonSlider/components/Labels.svelte b/src/shared/ui/ComparisonSlider/components/Labels.svelte new file mode 100644 index 0000000..0c01646 --- /dev/null +++ b/src/shared/ui/ComparisonSlider/components/Labels.svelte @@ -0,0 +1,37 @@ + + + +
+ +
+ Baseline + + {fontB.name} + +
+ + +
90 ? 0 : 1} + > + Comparison + + {fontA.name} + +
+
diff --git a/src/shared/ui/ComparisonSlider/components/SliderLine.svelte b/src/shared/ui/ComparisonSlider/components/SliderLine.svelte new file mode 100644 index 0000000..749ab3d --- /dev/null +++ b/src/shared/ui/ComparisonSlider/components/SliderLine.svelte @@ -0,0 +1,26 @@ + + + +
+ +
+
+ + +
+
+
+ + +
+
+
+
diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 307dcc6..bb4bf5b 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -6,6 +6,7 @@ import CheckboxFilter from './CheckboxFilter/CheckboxFilter.svelte'; import ComboControl from './ComboControl/ComboControl.svelte'; +import ComparisonSlider from './ComparisonSlider/ComparisonSlider.svelte'; import ContentEditable from './ContentEditable/ContentEditable.svelte'; import SearchBar from './SearchBar/SearchBar.svelte'; import VirtualList from './VirtualList/VirtualList.svelte'; @@ -13,6 +14,7 @@ import VirtualList from './VirtualList/VirtualList.svelte'; export { CheckboxFilter, ComboControl, + ComparisonSlider, ContentEditable, SearchBar, VirtualList, -- 2.49.1 From 1b7628423741d2c5640cea2102e37e438c55b7b2 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 20 Jan 2026 09:33:57 +0300 Subject: [PATCH 002/129] feat(PairSelector): implement PairSelector component that allows to choose the pair of fonts to compare --- .../DisplayFont/ui/FontPair/FontPair.svelte | 29 ++++++++++++++++ .../ui/PairSelector/PairSelector.svelte | 34 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/features/DisplayFont/ui/FontPair/FontPair.svelte create mode 100644 src/features/DisplayFont/ui/PairSelector/PairSelector.svelte diff --git a/src/features/DisplayFont/ui/FontPair/FontPair.svelte b/src/features/DisplayFont/ui/FontPair/FontPair.svelte new file mode 100644 index 0000000..b99626d --- /dev/null +++ b/src/features/DisplayFont/ui/FontPair/FontPair.svelte @@ -0,0 +1,29 @@ + + +
+ + {font1.name} + + vs + + {font2.name} + +
diff --git a/src/features/DisplayFont/ui/PairSelector/PairSelector.svelte b/src/features/DisplayFont/ui/PairSelector/PairSelector.svelte new file mode 100644 index 0000000..d3fb5d8 --- /dev/null +++ b/src/features/DisplayFont/ui/PairSelector/PairSelector.svelte @@ -0,0 +1,34 @@ + + + + + {triggerContent} + + + + {#snippet children({ item: pair })} + + {/snippet} + + + -- 2.49.1 From 746a377038f3dc0a6514d9f170fba3ece11aa8c5 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 20 Jan 2026 09:35:44 +0300 Subject: [PATCH 003/129] feat(FontVirtualList): add font pairs support --- .../Font/ui/FontVirtualList/FontVirtualList.svelte | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte b/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte index 65102db..6c6eb36 100644 --- a/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte +++ b/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte @@ -3,7 +3,7 @@ - Renders a virtualized list of fonts - Handles font registration with the manager --> - + +{#if hasAnyPairs} +
+
+ + +
+ + {#if fontA && fontB} + + {/if} +
+{/if} diff --git a/src/features/DisplayFont/ui/FontDisplay/FontDisplay.svelte b/src/features/DisplayFont/ui/FontDisplay/FontDisplay.svelte index 915078f..3aea03a 100644 --- a/src/features/DisplayFont/ui/FontDisplay/FontDisplay.svelte +++ b/src/features/DisplayFont/ui/FontDisplay/FontDisplay.svelte @@ -2,11 +2,14 @@ Component: FontDisplay Displays a grid of FontSampler components for each displayed font. --> - + +
{#each displayedFontsStore.fonts as font (font.id)} -- 2.49.1 From 55a560b78531d9a4b4216671449261b0174a0b8f Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 20 Jan 2026 14:17:41 +0300 Subject: [PATCH 007/129] feat(appliedFontsStore): implement the logic to update font link when font weight changes --- .../appliedFontsStore.svelte.ts | 130 +++++++++++------- .../components/SliderLine.svelte | 51 +++++-- 2 files changed, 118 insertions(+), 63 deletions(-) diff --git a/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts b/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts index 3394b6f..169877a 100644 --- a/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts +++ b/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts @@ -2,66 +2,83 @@ import { SvelteMap } from 'svelte/reactivity'; export type FontStatus = 'loading' | 'loaded' | 'error'; +export interface FontConfigRequest { + slug: string; + weight: number; + isVariable?: boolean; +} + /** - * Manager that handles loading of the fonts - * Adds tags to - * - Uses batch loading to reduce the number of requests - * - Uses a queue to prevent too many requests at once - * - Purges unused fonts after a certain time + * Manager that handles loading of fonts from Fontshare. + * Logic: + * - Variable fonts: Loaded once per slug (covers all weights). + * - Static fonts: Loaded per slug + weight combination. */ class AppliedFontsManager { - // Stores: slug -> timestamp of last visibility + // Tracking usage: Map where key is "slug" or "slug@weight" #usageTracker = new Map(); - // Stores: slug -> batchId + // Map: key -> batchId #slugToBatch = new Map(); - // Stores: batchId -> HTMLLinkElement (for physical cleanup) + // Map: batchId -> HTMLLinkElement #batchElements = new Map(); #queue = new Set(); #timeoutId: ReturnType | null = null; - #PURGE_INTERVAL = 60000; // Check every minute - #TTL = 5 * 60 * 1000; // 5 minutes + + #PURGE_INTERVAL = 60000; + #TTL = 5 * 60 * 1000; #CHUNK_SIZE = 3; + // Reactive status map for UI feedback statuses = new SvelteMap(); constructor() { if (typeof window !== 'undefined') { - // Start the "Janitor" loop setInterval(() => this.#purgeUnused(), this.#PURGE_INTERVAL); } } /** - * Updates the 'last seen' timestamp for fonts. - * Prevents them from being purged while they are on screen. + * Resolves a unique key for the font asset. */ - touch(slugs: string[]) { + #getFontKey(slug: string, weight: number, isVariable: boolean): string { + const s = slug.toLowerCase(); + // Variable fonts only need one entry regardless of weight + return isVariable ? s : `${s}@${weight}`; + } + + /** + * Call this when a font is rendered on screen. + */ + touch(configs: FontConfigRequest[]) { const now = Date.now(); const toRegister: string[] = []; - slugs.forEach(slug => { - this.#usageTracker.set(slug, now); - if (!this.#slugToBatch.has(slug)) { - toRegister.push(slug); + configs.forEach(({ slug, weight, isVariable = false }) => { + const key = this.#getFontKey(slug, weight, isVariable); + + this.#usageTracker.set(key, now); + + if (!this.#slugToBatch.has(key)) { + toRegister.push(key); } }); if (toRegister.length > 0) this.registerFonts(toRegister); } - registerFonts(slugs: string[]) { - const newSlugs = slugs.filter(s => !this.#slugToBatch.has(s) && !this.#queue.has(s)); - if (newSlugs.length === 0) return; + registerFonts(keys: string[]) { + const newKeys = keys.filter(k => !this.#slugToBatch.has(k) && !this.#queue.has(k)); + if (newKeys.length === 0) return; - newSlugs.forEach(s => this.#queue.add(s)); + newKeys.forEach(k => this.#queue.add(k)); if (this.#timeoutId) clearTimeout(this.#timeoutId); this.#timeoutId = setTimeout(() => this.#processQueue(), 50); } - getFontStatus(slug: string) { - return this.statuses.get(slug); + getFontStatus(slug: string, weight: number, isVariable: boolean) { + return this.statuses.get(this.#getFontKey(slug, weight, isVariable)); } #processQueue() { @@ -76,16 +93,23 @@ class AppliedFontsManager { this.#timeoutId = null; } - #createBatch(slugs: string[]) { + #createBatch(keys: string[]) { if (typeof document === 'undefined') return; const batchId = crypto.randomUUID(); - // font-display=swap included for better UX - const query = slugs.map(s => `f[]=${s.toLowerCase()}@400`).join('&'); + + /** + * Fontshare API Logic: + * - If key contains '@', it's static (e.g., satoshi@700) + * - If it's a plain slug, it's variable. We append '@1,2' for variable assets. + */ + const query = keys.map(k => { + return k.includes('@') ? `f[]=${k}` : `f[]=${k}@1,2`; + }).join('&'); + const url = `https://api.fontshare.com/v2/css?${query}&display=swap`; - // Mark all as loading immediately - slugs.forEach(slug => this.statuses.set(slug, 'loading')); + keys.forEach(key => this.statuses.set(key, 'loading')); const link = document.createElement('link'); link.rel = 'stylesheet'; @@ -94,21 +118,24 @@ class AppliedFontsManager { document.head.appendChild(link); this.#batchElements.set(batchId, link); - slugs.forEach(slug => { - this.#slugToBatch.set(slug, batchId); - // Use the Native Font Loading API - // format: "font-size font-family" - document.fonts.load(`1em "${slug}"`) + keys.forEach(key => { + this.#slugToBatch.set(key, batchId); + + // Determine what to check in the Font Loading API + const isVariable = !key.includes('@'); + const [family, staticWeight] = key.split('@'); + + // For variable fonts, we check a standard weight; + // for static, we check the specific numeric weight requested. + const weightToCheck = isVariable ? '400' : staticWeight; + + document.fonts.load(`${weightToCheck} 1em "${family}"`) .then(loadedFonts => { - if (loadedFonts.length > 0) { - this.statuses.set(slug, 'loaded'); - } else { - this.statuses.set(slug, 'error'); - } + this.statuses.set(key, loadedFonts.length > 0 ? 'loaded' : 'error'); }) .catch(() => { - this.statuses.set(slug, 'error'); + this.statuses.set(key, 'error'); }); }); } @@ -116,31 +143,30 @@ class AppliedFontsManager { #purgeUnused() { const now = Date.now(); const batchesToPotentialDelete = new Set(); - const slugsToDelete: string[] = []; + const keysToDelete: string[] = []; - // Identify expired slugs - for (const [slug, lastUsed] of this.#usageTracker.entries()) { + for (const [key, lastUsed] of this.#usageTracker.entries()) { if (now - lastUsed > this.#TTL) { - const batchId = this.#slugToBatch.get(slug); + const batchId = this.#slugToBatch.get(key); if (batchId) batchesToPotentialDelete.add(batchId); - slugsToDelete.push(slug); + keysToDelete.push(key); } } - // Only remove a batch if ALL fonts in that batch are expired batchesToPotentialDelete.forEach(batchId => { - const batchSlugs = Array.from(this.#slugToBatch.entries()) + const batchKeys = Array.from(this.#slugToBatch.entries()) .filter(([_, bId]) => bId === batchId) - .map(([slug]) => slug); + .map(([key]) => key); - const allExpired = batchSlugs.every(s => slugsToDelete.includes(s)); + const allExpired = batchKeys.every(k => keysToDelete.includes(k)); if (allExpired) { this.#batchElements.get(batchId)?.remove(); this.#batchElements.delete(batchId); - batchSlugs.forEach(s => { - this.#slugToBatch.delete(s); - this.#usageTracker.delete(s); + batchKeys.forEach(k => { + this.#slugToBatch.delete(k); + this.#usageTracker.delete(k); + this.statuses.delete(k); }); } }); diff --git a/src/shared/ui/ComparisonSlider/components/SliderLine.svelte b/src/shared/ui/ComparisonSlider/components/SliderLine.svelte index 749ab3d..47f1375 100644 --- a/src/shared/ui/ComparisonSlider/components/SliderLine.svelte +++ b/src/shared/ui/ComparisonSlider/components/SliderLine.svelte @@ -1,26 +1,55 @@ -
- -
+ +
- -
-
+ +
- -
-
+ +
+
+
+ + +
+
+ + -- 2.49.1 From d4d2d68d9ae46812e01d67bf0b74f5f7d6e2b2c3 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 20 Jan 2026 14:21:07 +0300 Subject: [PATCH 008/129] feat(appliedFontsStore): incorporate implemented font weight logic --- .../ui/FontApplicator/FontApplicator.svelte | 10 +++++--- .../ui/FontComparer/FontComparer.svelte | 11 ++++++-- .../ui/FontSampler/FontSampler.svelte | 4 +++ .../createCharacterComparison.svelte.ts | 25 +++++++++---------- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/entities/Font/ui/FontApplicator/FontApplicator.svelte b/src/entities/Font/ui/FontApplicator/FontApplicator.svelte index 15dd0d9..f724e94 100644 --- a/src/entities/Font/ui/FontApplicator/FontApplicator.svelte +++ b/src/entities/Font/ui/FontApplicator/FontApplicator.svelte @@ -20,6 +20,10 @@ interface Props { * Font id to load */ id: string; + /** + * Font weight + */ + weight?: number; /** * Additional classes */ @@ -30,7 +34,7 @@ interface Props { children?: Snippet; } -let { name, id, className, children }: Props = $props(); +let { name, id, weight = 400, className, children }: Props = $props(); let element: Element; // Track if the user has actually scrolled this into view @@ -40,7 +44,7 @@ $effect(() => { const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { hasEnteredViewport = true; - appliedFontsManager.touch([id]); + appliedFontsManager.touch([{ slug: id, weight }]); // Once it has entered, we can stop observing to save CPU observer.unobserve(element); @@ -50,7 +54,7 @@ $effect(() => { return () => observer.disconnect(); }); -const status = $derived(appliedFontsManager.getFontStatus(id)); +const status = $derived(appliedFontsManager.getFontStatus(id, weight, false)); // The "Show" condition: Element is in view AND (Font is ready OR it errored out) const shouldReveal = $derived(hasEnteredViewport && (status === 'loaded' || status === 'error')); diff --git a/src/features/DisplayFont/ui/FontComparer/FontComparer.svelte b/src/features/DisplayFont/ui/FontComparer/FontComparer.svelte index 7a3238b..287f206 100644 --- a/src/features/DisplayFont/ui/FontComparer/FontComparer.svelte +++ b/src/features/DisplayFont/ui/FontComparer/FontComparer.svelte @@ -1,5 +1,6 @@ @@ -22,7 +23,13 @@ $effect(() => {
{#if fontA && fontB} - + +
{/if}
{/if} diff --git a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte index 872e11c..fa506f2 100644 --- a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte +++ b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte @@ -7,6 +7,7 @@ import { FontApplicator, type UnifiedFont, } from '$entities/Font'; +import { controlManager } from '$features/SetupFont'; import { ContentEditable } from '$shared/ui'; interface Props { @@ -31,6 +32,8 @@ let { text = $bindable(), ...restProps }: Props = $props(); + +const weight = $derived(controlManager.weight ?? 400);
diff --git a/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts b/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts index 6c2f458..6eca284 100644 --- a/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts +++ b/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts @@ -19,6 +19,7 @@ export function createCharacterComparison( text: () => string, fontA: () => { name: string; id: string }, fontB: () => { name: string; id: string }, + weight: () => number, ) { let lines = $state([]); let containerWidth = $state(0); @@ -29,14 +30,16 @@ export function createCharacterComparison( * @param text - Text string to measure * @param fontFamily - Font family name * @param fontSize - Font size in pixels + * @param fontWeight - Font weight */ function measureText( ctx: CanvasRenderingContext2D, text: string, fontFamily: string, fontSize: number, + fontWeight: number, ): number { - ctx.font = `bold ${fontSize}px ${fontFamily}`; + ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`; return ctx.measureText(text).width; } @@ -62,6 +65,7 @@ export function createCharacterComparison( * @param container - The container element to measure width from * @param measureCanvas - The canvas element used for text measurement */ + function breakIntoLines( container: HTMLElement | undefined, measureCanvas: HTMLCanvasElement | undefined, @@ -70,15 +74,14 @@ export function createCharacterComparison( const rect = container.getBoundingClientRect(); containerWidth = rect.width; - // Padding considerations - matches the container padding const padding = window.innerWidth < 640 ? 48 : 96; const availableWidth = rect.width - padding; - const ctx = measureCanvas.getContext('2d'); if (!ctx) return; const fontSize = getFontSize(); + const currentWeight = weight(); // Get current weight const words = text().split(' '); const newLines: LineData[] = []; let currentLineWords: string[] = []; @@ -86,9 +89,9 @@ export function createCharacterComparison( function pushLine(words: string[]) { if (words.length === 0) return; const lineText = words.join(' '); - // Measure width to ensure we know exactly how wide this line renders - const widthA = measureText(ctx!, lineText, fontA().name, fontSize); - const widthB = measureText(ctx!, lineText, fontB().name, fontSize); + // Measure both fonts at the CURRENT weight + const widthA = measureText(ctx!, lineText, fontA().name, fontSize, currentWeight); + const widthB = measureText(ctx!, lineText, fontB().name, fontSize, currentWeight); const maxWidth = Math.max(widthA, widthB); newLines.push({ text: lineText, width: maxWidth }); } @@ -97,10 +100,9 @@ export function createCharacterComparison( const testLine = currentLineWords.length > 0 ? currentLineWords.join(' ') + ' ' + word : word; - // Measure with both fonts and use the wider one to prevent layout shifts - const widthA = measureText(ctx, testLine, fontA().name, fontSize); - const widthB = measureText(ctx, testLine, fontB().name, fontSize); + const widthA = measureText(ctx, testLine, fontA().name, fontSize, currentWeight); + const widthB = measureText(ctx, testLine, fontB().name, fontSize, currentWeight); const maxWidth = Math.max(widthA, widthB); if (maxWidth > availableWidth && currentLineWords.length > 0) { @@ -111,10 +113,7 @@ export function createCharacterComparison( } } - if (currentLineWords.length > 0) { - pushLine(currentLineWords); - } - + if (currentLineWords.length > 0) pushLine(currentLineWords); lines = newLines; } -- 2.49.1 From a0f184665d0483b75f299c9efb0fccd2c30b2f0b Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 20 Jan 2026 14:23:58 +0300 Subject: [PATCH 009/129] feat(ComparisonSlider): Improve Comparison slider's readability, incapsulate some code into separate components and snippets --- src/shared/lib/helpers/index.ts | 1 + src/shared/lib/index.ts | 1 + .../ComparisonSlider/ComparisonSlider.svelte | 100 +++++++++--------- .../components/CharacterSlot.svelte | 54 ++++++++++ 4 files changed, 107 insertions(+), 49 deletions(-) create mode 100644 src/shared/ui/ComparisonSlider/components/CharacterSlot.svelte diff --git a/src/shared/lib/helpers/index.ts b/src/shared/lib/helpers/index.ts index 61b065f..a180a14 100644 --- a/src/shared/lib/helpers/index.ts +++ b/src/shared/lib/helpers/index.ts @@ -29,4 +29,5 @@ export { export { createCharacterComparison, + type LineData, } from './createCharacterComparison/createCharacterComparison.svelte'; diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index 9734dd5..35dbe09 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -11,6 +11,7 @@ export { type EntityStore, type Filter, type FilterModel, + type LineData, type Property, type TypographyControl, type VirtualItem, diff --git a/src/shared/ui/ComparisonSlider/ComparisonSlider.svelte b/src/shared/ui/ComparisonSlider/ComparisonSlider.svelte index 00d493b..1692630 100644 --- a/src/shared/ui/ComparisonSlider/ComparisonSlider.svelte +++ b/src/shared/ui/ComparisonSlider/ComparisonSlider.svelte @@ -11,7 +11,9 @@ --> +{#snippet renderLine(line: LineData, lineIndex: number)} +
+ {#each line.text.split('') as char, charIndex} + {@const { proximity, isPast } = charComparison.getCharState(lineIndex, charIndex, line, sliderPos)} + + + {/each} +
+{/snippet} + @@ -110,7 +140,11 @@ $effect(() => { aria-valuenow={Math.round(sliderPos)} aria-label="Font comparison slider" onpointerdown={startDragging} - class="group relative w-full py-16 px-6 sm:py-24 sm:px-12 overflow-hidden bg-white rounded-[2.5rem] border border-slate-100 shadow-2xl select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center" + class=" + group relative w-full py-16 px-6 sm:py-24 sm:px-12 overflow-hidden + bg-white rounded-[2.5rem] border border-slate-100 shadow-2xl + select-none touch-none cursor-ew-resize min-h-100 flex flex-col justify-center + " >
{
{#each charComparison.lines as line, lineIndex} -
- {#each line.text.split('') as char, charIndex} - {@const { proximity, isPast } = charComparison.getCharState(lineIndex, charIndex, line, sliderPos)} - - 0 - ? 'transform, font-family, color' - : 'auto'} - > - {char === ' ' ? '\u00A0' : char} - - {/each} +
+ {@render renderLine(line, lineIndex)}
{/each}
- +
- - diff --git a/src/shared/ui/ComparisonSlider/components/CharacterSlot.svelte b/src/shared/ui/ComparisonSlider/components/CharacterSlot.svelte new file mode 100644 index 0000000..7546bee --- /dev/null +++ b/src/shared/ui/ComparisonSlider/components/CharacterSlot.svelte @@ -0,0 +1,54 @@ + + + + 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} + + + -- 2.49.1 From c6d20aae3d22ea3e717d86f88e643dde379e9309 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 21 Jan 2026 21:50:30 +0300 Subject: [PATCH 010/129] feat(ComboControlV2): crete ComboControlV2 - without increase/decrease buttons. Refresh styling of the original one --- .../ui/ComboControl/ComboControl.svelte | 37 ++++++---- .../ui/ComboControlV2/ComboControlV2.svelte | 72 +++++++++++++++++++ src/shared/ui/index.ts | 4 +- 3 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 src/shared/ui/ComboControlV2/ComboControlV2.svelte diff --git a/src/shared/ui/ComboControl/ComboControl.svelte b/src/shared/ui/ComboControl/ComboControl.svelte index c567d13..c226047 100644 --- a/src/shared/ui/ComboControl/ComboControl.svelte +++ b/src/shared/ui/ComboControl/ComboControl.svelte @@ -8,9 +8,13 @@ - + - - + + {#snippet child({ props })} {/snippet} - - + +
{ class="w-16 text-center" />
-
-
+ + -
+ diff --git a/src/shared/ui/ComboControlV2/ComboControlV2.svelte b/src/shared/ui/ComboControlV2/ComboControlV2.svelte new file mode 100644 index 0000000..7a8f54d --- /dev/null +++ b/src/shared/ui/ComboControlV2/ComboControlV2.svelte @@ -0,0 +1,72 @@ + + + +
+ + +
diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index bb4bf5b..86fcf01 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -6,7 +6,7 @@ import CheckboxFilter from './CheckboxFilter/CheckboxFilter.svelte'; import ComboControl from './ComboControl/ComboControl.svelte'; -import ComparisonSlider from './ComparisonSlider/ComparisonSlider.svelte'; +import ComboControlV2 from './ComboControlV2/ComboControlV2.svelte'; import ContentEditable from './ContentEditable/ContentEditable.svelte'; import SearchBar from './SearchBar/SearchBar.svelte'; import VirtualList from './VirtualList/VirtualList.svelte'; @@ -14,7 +14,7 @@ import VirtualList from './VirtualList/VirtualList.svelte'; export { CheckboxFilter, ComboControl, - ComparisonSlider, + ComboControlV2, ContentEditable, SearchBar, VirtualList, -- 2.49.1 From 2ee66316f770e605f7a08ff68c89dc5745b12742 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 21 Jan 2026 21:51:22 +0300 Subject: [PATCH 011/129] chore(controlManager): rewrite controlManager to classes --- .../controlManager/controlManager.svelte.ts | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts b/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts index 3307c4c..474dab1 100644 --- a/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts +++ b/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts @@ -1,7 +1,49 @@ import { type ControlModel, + type TypographyControl, createTypographyControl, } from '$shared/lib'; +import { SvelteMap } from 'svelte/reactivity'; + +export interface Control { + id: string; + increaseLabel?: string; + decreaseLabel?: string; + controlLabel?: string; + instance: TypographyControl; +} + +export class TypographyControlManager { + #controls = new SvelteMap(); + + constructor(configs: ControlModel[]) { + configs.forEach(({ id, increaseLabel, decreaseLabel, controlLabel, ...config }) => { + this.#controls.set(id, { + id, + increaseLabel, + decreaseLabel, + controlLabel, + instance: createTypographyControl(config), + }); + }); + } + + get controls() { + return this.#controls.values(); + } + + get weight() { + return this.#controls.get('font_weight')?.instance.value ?? 400; + } + + get size() { + return this.#controls.get('font_size')?.instance.value; + } + + get height() { + return this.#controls.get('line_height')?.instance.value; + } +} /** * Creates a typography control manager that handles a collection of typography controls. @@ -10,19 +52,5 @@ import { * @returns - Typography control manager instance. */ export function createTypographyControlManager(configs: ControlModel[]) { - const controls = $state( - configs.map(({ id, increaseLabel, decreaseLabel, controlLabel, ...config }) => ({ - id, - increaseLabel, - decreaseLabel, - controlLabel, - instance: createTypographyControl(config), - })), - ); - - return { - get controls() { - return controls; - }, - }; + return new TypographyControlManager(configs); } -- 2.49.1 From 91300bdc2513eba57e354b39dabc2debb544ba49 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 21 Jan 2026 21:52:55 +0300 Subject: [PATCH 012/129] feat(ComparisonSlider): Massively improve the slider and move it to the widgets layer --- .../ComparisonSlider/ComparisonSlider.svelte | 169 ++++++++---- .../components/CharacterSlot.svelte | 25 +- .../components/ControlsWrapper.svelte | 241 ++++++++++++++++++ .../ComparisonSlider/components/Labels.svelte | 0 .../components/SliderLine.svelte | 0 src/widgets/ComparisonSlider/ui/index.ts | 3 + 6 files changed, 382 insertions(+), 56 deletions(-) rename src/{shared => widgets/ComparisonSlider}/ui/ComparisonSlider/ComparisonSlider.svelte (52%) rename src/{shared => widgets/ComparisonSlider}/ui/ComparisonSlider/components/CharacterSlot.svelte (72%) create mode 100644 src/widgets/ComparisonSlider/ui/ComparisonSlider/components/ControlsWrapper.svelte rename src/{shared => widgets/ComparisonSlider}/ui/ComparisonSlider/components/Labels.svelte (100%) rename src/{shared => widgets/ComparisonSlider}/ui/ComparisonSlider/components/SliderLine.svelte (100%) create mode 100644 src/widgets/ComparisonSlider/ui/index.ts diff --git a/src/shared/ui/ComparisonSlider/ComparisonSlider.svelte b/src/widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.svelte similarity index 52% rename from src/shared/ui/ComparisonSlider/ComparisonSlider.svelte rename to src/widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.svelte index 1692630..e259a97 100644 --- a/src/shared/ui/ComparisonSlider/ComparisonSlider.svelte +++ b/src/widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.svelte @@ -10,35 +10,64 @@ - Performance optimized using offscreen canvas for measurements and transform-based animations. --> +import type { TypographyControl } from '$shared/lib'; +import { Input } from '$shared/shadcn/ui/input'; +import { cn } from '$shared/shadcn/utils/shadcn-utils'; +import { ComboControlV2 } from '$shared/ui'; +import AArrowUP from '@lucide/svelte/icons/a-arrow-up'; +import { Spring } from 'svelte/motion'; +import { slide } from 'svelte/transition'; + +interface Props { + wrapper?: HTMLDivElement | null; + sliderPos: number; + isDragging: boolean; + text: string; + containerWidth: number; + weightControl: TypographyControl; + sizeControl: TypographyControl; + heightControl: TypographyControl; +} + +let { + sliderPos, + isDragging, + wrapper = $bindable(null), + text = $bindable(), + containerWidth = 0, + weightControl, + sizeControl, + heightControl, +}: Props = $props(); + +let panelWidth = $state(0); +const margin = 24; +let side = $state<'left' | 'right'>('left'); + +// Unified active state for the entire wrapper +let isActive = $state(false); + +function handleWrapperClick() { + if (!isDragging) { + isActive = true; + } +} + +function handleClickOutside(e: MouseEvent) { + if (wrapper && !wrapper.contains(e.target as Node)) { + isActive = false; + } +} + +function handleInputFocus() { + isActive = true; +} + +function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleWrapperClick(); + } +} + +// Movement Logic +$effect(() => { + if (containerWidth === 0 || panelWidth === 0) return; + const sliderX = (sliderPos / 100) * containerWidth; + const buffer = 40; + const leftTrigger = margin + panelWidth + buffer; + const rightTrigger = containerWidth - (margin + panelWidth + buffer); + + if (side === 'left' && sliderX < leftTrigger) { + side = 'right'; + } else if (side === 'right' && sliderX > rightTrigger) { + side = 'left'; + } +}); + +// The "Dodge" +const xSpring = new Spring(0, { + stiffness: 0.14, // Lower is slower + damping: 0.5, // Settle +}); + +// The "Focus" +const ySpring = new Spring(0, { + stiffness: 0.32, + damping: 0.65, +}); + +// The "Rise" +const scaleSpring = new Spring(1, { + stiffness: 0.32, + damping: 0.65, +}); + +// The "Lean" +const rotateSpring = new Spring(0, { + stiffness: 0.12, + damping: 0.55, +}); + +$effect(() => { + const targetX = side === 'right' ? containerWidth - panelWidth - margin * 2 : 0; + if (containerWidth > 0 && panelWidth > 0 && !isActive) { + // On side change set the position and the rotation + xSpring.target = targetX; + rotateSpring.target = side === 'right' ? 3.5 : -3.5; + + setTimeout(() => { + rotateSpring.target = 0; + }, 600); + } +}); + +// Elevation and scale on focus and mouse over +$effect(() => { + if (isActive && !isDragging) { + // Lift up + ySpring.target = 8; + // Slightly bigger + scaleSpring.target = 1.1; + + rotateSpring.target = side === 'right' ? -1.1 : 1.1; + + setTimeout(() => { + rotateSpring.target = 0; + scaleSpring.target = 1.05; + }, 300); + } else { + ySpring.target = 0; + scaleSpring.target = 1; + rotateSpring.target = 0; + } +}); + +$effect(() => { + if (isDragging) { + isActive = false; + } +}); + +// Click outside handler +$effect(() => { + if (typeof window === 'undefined') return; + + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); +}); + + +
+
+ +
+ +
+
+ +
+ + {#if isActive} +
+ + + +
+ {/if} +
+
+ + diff --git a/src/shared/ui/ComparisonSlider/components/Labels.svelte b/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/Labels.svelte similarity index 100% rename from src/shared/ui/ComparisonSlider/components/Labels.svelte rename to src/widgets/ComparisonSlider/ui/ComparisonSlider/components/Labels.svelte diff --git a/src/shared/ui/ComparisonSlider/components/SliderLine.svelte b/src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SliderLine.svelte similarity index 100% rename from src/shared/ui/ComparisonSlider/components/SliderLine.svelte rename to src/widgets/ComparisonSlider/ui/ComparisonSlider/components/SliderLine.svelte diff --git a/src/widgets/ComparisonSlider/ui/index.ts b/src/widgets/ComparisonSlider/ui/index.ts new file mode 100644 index 0000000..ccad21a --- /dev/null +++ b/src/widgets/ComparisonSlider/ui/index.ts @@ -0,0 +1,3 @@ +import ComparisonSlider from './ComparisonSlider/ComparisonSlider.svelte'; + +export { ComparisonSlider }; -- 2.49.1 From 46de3c6e87fc1e8aa157225724fc42261e5139b1 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 21 Jan 2026 21:54:48 +0300 Subject: [PATCH 013/129] chore(createTypographyControl): make some props optional --- .../createTypographyControl.svelte.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/lib/helpers/createTypographyControl/createTypographyControl.svelte.ts b/src/shared/lib/helpers/createTypographyControl/createTypographyControl.svelte.ts index 1ba9476..bec0f1d 100644 --- a/src/shared/lib/helpers/createTypographyControl/createTypographyControl.svelte.ts +++ b/src/shared/lib/helpers/createTypographyControl/createTypographyControl.svelte.ts @@ -30,15 +30,15 @@ export interface ControlModel extends ControlDataModel { /** * Area label for increase button */ - increaseLabel: string; + increaseLabel?: string; /** * Area label for decrease button */ - decreaseLabel: string; + decreaseLabel?: string; /** * Control area label */ - controlLabel: string; + controlLabel?: string; } export function createTypographyControl( -- 2.49.1 From a5380333eb81a552a6cf78d273157dfdfef40f8f Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 21 Jan 2026 21:56:34 +0300 Subject: [PATCH 014/129] feat(createCharacterComparison): add support for font size change --- .../createCharacterComparison.svelte.ts | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts b/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts index 6eca284..3b0a596 100644 --- a/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts +++ b/src/shared/lib/helpers/createCharacterComparison/createCharacterComparison.svelte.ts @@ -20,6 +20,7 @@ export function createCharacterComparison( fontA: () => { name: string; id: string }, fontB: () => { name: string; id: string }, weight: () => number, + size: () => number, ) { let lines = $state([]); let containerWidth = $state(0); @@ -48,7 +49,13 @@ export function createCharacterComparison( * Matches the Tailwind breakpoints used in the component. */ function getFontSize() { - if (typeof window === 'undefined') return 64; + // const fontSize = size(); + // if (fontSize) { + // return fontSize; + // } + if (typeof window === 'undefined') { + return 64; + } return window.innerWidth >= 1024 ? 112 : window.innerWidth >= 768 @@ -80,6 +87,7 @@ export function createCharacterComparison( const ctx = measureCanvas.getContext('2d'); if (!ctx) return; + const controlledFontSize = size(); const fontSize = getFontSize(); const currentWeight = weight(); // Get current weight const words = text().split(' '); @@ -90,8 +98,20 @@ export function createCharacterComparison( if (words.length === 0) return; const lineText = words.join(' '); // Measure both fonts at the CURRENT weight - const widthA = measureText(ctx!, lineText, fontA().name, fontSize, currentWeight); - const widthB = measureText(ctx!, lineText, fontB().name, fontSize, currentWeight); + const widthA = measureText( + ctx!, + lineText, + fontA().name, + Math.min(fontSize, controlledFontSize), + currentWeight, + ); + const widthB = measureText( + ctx!, + lineText, + fontB().name, + Math.min(fontSize, controlledFontSize), + currentWeight, + ); const maxWidth = Math.max(widthA, widthB); newLines.push({ text: lineText, width: maxWidth }); } @@ -101,8 +121,20 @@ export function createCharacterComparison( ? currentLineWords.join(' ') + ' ' + word : word; // Measure with both fonts and use the wider one to prevent layout shifts - const widthA = measureText(ctx, testLine, fontA().name, fontSize, currentWeight); - const widthB = measureText(ctx, testLine, fontB().name, fontSize, currentWeight); + const widthA = measureText( + ctx, + testLine, + fontA().name, + Math.min(fontSize, controlledFontSize), + currentWeight, + ); + const widthB = measureText( + ctx, + testLine, + fontB().name, + Math.min(fontSize, controlledFontSize), + currentWeight, + ); const maxWidth = Math.max(widthA, widthB); if (maxWidth > availableWidth && currentLineWords.length > 0) { -- 2.49.1 From 1d0ca31262c2a9621ac3b5cf6cf85709629cf509 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 21 Jan 2026 21:57:04 +0300 Subject: [PATCH 015/129] chore: input path change --- .../DisplayFont/ui/FontComparer/FontComparer.svelte | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/DisplayFont/ui/FontComparer/FontComparer.svelte b/src/features/DisplayFont/ui/FontComparer/FontComparer.svelte index 287f206..198b90e 100644 --- a/src/features/DisplayFont/ui/FontComparer/FontComparer.svelte +++ b/src/features/DisplayFont/ui/FontComparer/FontComparer.svelte @@ -2,7 +2,7 @@ import { appliedFontsManager } from '$entities/Font'; import { controlManager } from '$features/SetupFont'; import { Input } from '$shared/shadcn/ui/input'; -import { ComparisonSlider } from '$shared/ui'; +import { ComparisonSlider } from '$widgets/ComparisonSlider/ui'; import { displayedFontsStore } from '../../model'; import PairSelector from '../PairSelector/PairSelector.svelte'; @@ -11,7 +11,9 @@ const [fontA, fontB] = $derived(displayedFontsStore.selectedPair); const hasAnyPairs = $derived(displayedFontsStore.fonts.length > 0); $effect(() => { - appliedFontsManager.touch(displayedFontsStore.fonts.map(font => ({ slug: font.id, weight }))); + appliedFontsManager.touch( + displayedFontsStore.fonts.map(font => ({ slug: font.id, weight: controlManager.weight })), + ); }); @@ -26,10 +28,8 @@ $effect(() => { -
{/if}
{/if} -- 2.49.1 From b41c48da67420a5673c18d93835570f2af72ad2f Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 22 Jan 2026 15:31:59 +0300 Subject: [PATCH 016/129] feat(app): change main font --- src/app/styles/app.css | 2 ++ src/app/ui/Layout.svelte | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/app/styles/app.css b/src/app/styles/app.css index f574f19..2461f0d 100644 --- a/src/app/styles/app.css +++ b/src/app/styles/app.css @@ -117,6 +117,8 @@ } body { @apply bg-background text-foreground; + font-family: 'Karla', system-ui, sans-serif; + font-optical-sizing: auto; } } diff --git a/src/app/ui/Layout.svelte b/src/app/ui/Layout.svelte index 1764df0..f3b6872 100644 --- a/src/app/ui/Layout.svelte +++ b/src/app/ui/Layout.svelte @@ -26,6 +26,13 @@ let { children } = $props(); + + + +
-- 2.49.1 From e4970e43baf7542d7907e9144cdbee071b8aef5e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 22 Jan 2026 15:33:38 +0300 Subject: [PATCH 017/129] chore: switch to use of svelte native prefersReducedMotion media --- .../ui/FontApplicator/FontApplicator.svelte | 9 +++--- src/shared/lib/accessibility/motion.svelte.ts | 32 ------------------- src/shared/lib/index.ts | 1 - .../ui/CheckboxFilter/CheckboxFilter.svelte | 4 +-- 4 files changed, 7 insertions(+), 39 deletions(-) delete mode 100644 src/shared/lib/accessibility/motion.svelte.ts diff --git a/src/entities/Font/ui/FontApplicator/FontApplicator.svelte b/src/entities/Font/ui/FontApplicator/FontApplicator.svelte index f724e94..b5c8e5b 100644 --- a/src/entities/Font/ui/FontApplicator/FontApplicator.svelte +++ b/src/entities/Font/ui/FontApplicator/FontApplicator.svelte @@ -6,9 +6,9 @@ - Adds smooth transition when font appears --> - {#snippet children({ item: font })} - + {#snippet children({ item: font, isVisible, proximity })} + {/snippet} -- 2.49.1 From be13a5c8a075aa73f49680ccdc5d6b8855d2b40a Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 22 Jan 2026 15:40:37 +0300 Subject: [PATCH 025/129] feat(VirtualList): add proximity and isVisible props --- src/shared/ui/VirtualList/VirtualList.svelte | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/shared/ui/VirtualList/VirtualList.svelte b/src/shared/ui/VirtualList/VirtualList.svelte index cbc0ea1..719a957 100644 --- a/src/shared/ui/VirtualList/VirtualList.svelte +++ b/src/shared/ui/VirtualList/VirtualList.svelte @@ -52,7 +52,7 @@ interface Props { * * @template T - The type of items in the list */ - children: Snippet<[{ item: T; index: number }]>; + children: Snippet<[{ item: T; index: number; isVisible: boolean; proximity: number }]>; } let { items, itemHeight = 80, overscan = 5, class: className, onVisibleItemsChange, children }: @@ -76,8 +76,13 @@ $effect(() => { class={cn( 'relative overflow-auto rounded-md bg-background', 'h-150 w-full', + 'scroll-smooth', className, )} + onfocusin={(e => { + // Prevent the browser from jumping the scroll when an inner element gets focus + e.preventDefault(); + })} >
{ class="absolute top-0 left-0 w-full" style:transform="translateY({item.start}px)" > - {@render children({ item: items[item.index], index: item.index })} + {@render children({ + item: items[item.index], + index: item.index, + isVisible: item.isVisible, + proximity: item.proximity, +})}
{/each}
-- 2.49.1 From 59b0d9c620b22730ce47afbaf94ac7b4560a4a40 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 22 Jan 2026 15:41:55 +0300 Subject: [PATCH 026/129] feat(FontListItem): refactor component to enhance UX with animations and move away from checkboxes to avoid scroll problems --- .../Font/ui/FontListItem/FontListItem.svelte | 162 ++++++++++++------ 1 file changed, 106 insertions(+), 56 deletions(-) diff --git a/src/entities/Font/ui/FontListItem/FontListItem.svelte b/src/entities/Font/ui/FontListItem/FontListItem.svelte index 12c52a7..f32ec39 100644 --- a/src/entities/Font/ui/FontListItem/FontListItem.svelte +++ b/src/entities/Font/ui/FontListItem/FontListItem.svelte @@ -4,8 +4,8 @@ --> -
-