From 55a560b78531d9a4b4216671449261b0174a0b8f Mon Sep 17 00:00:00 2001
From: Ilia Mashkov
Date: Tue, 20 Jan 2026 14:17:41 +0300
Subject: [PATCH] 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 @@
-