Compare commits
7 Commits
2b7f21711b
...
1e2daa410c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e2daa410c | ||
|
|
adf6dc93ea | ||
|
|
596a023d24 | ||
|
|
8195e9baa8 | ||
|
|
0554fcada7 | ||
|
|
9a794b626b | ||
|
|
40346aa9aa |
592
src/entities/Font/lib/getFontUrl/getFontUrl.test.ts
Normal file
592
src/entities/Font/lib/getFontUrl/getFontUrl.test.ts
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
import {
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
} from 'vitest';
|
||||||
|
import type { UnifiedFont } from '../../model/types';
|
||||||
|
import { getFontUrl } from './getFontUrl';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a minimal UnifiedFont mock for testing
|
||||||
|
*/
|
||||||
|
function createMockFont(
|
||||||
|
overrides: Partial<UnifiedFont> = {},
|
||||||
|
): UnifiedFont {
|
||||||
|
const baseFont: UnifiedFont = {
|
||||||
|
id: 'test-font',
|
||||||
|
name: 'Test Font',
|
||||||
|
provider: 'google',
|
||||||
|
category: 'sans-serif',
|
||||||
|
subsets: ['latin'],
|
||||||
|
variants: [],
|
||||||
|
styles: {},
|
||||||
|
metadata: {
|
||||||
|
cachedAt: Date.now(),
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
isVariable: false,
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...baseFont, ...overrides };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('getFontUrl', () => {
|
||||||
|
describe('basic logic', () => {
|
||||||
|
it('returns URL for exact weight match in variants', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
'700': 'https://example.com/font-700.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 400);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-400.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns URL for weight 700', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'700': 'https://example.com/font-700.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 700);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-700.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns URL for weight 100 (lightest)', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'100': 'https://example.com/font-100.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 100);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-100.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns URL for weight 900 (boldest)', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'900': 'https://example.com/font-900.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 900);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-900.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns URL for variable font (backend maps weight to VF URL)', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-variable.woff2',
|
||||||
|
'700': 'https://example.com/font-variable.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result400 = getFontUrl(font, 400);
|
||||||
|
const result700 = getFontUrl(font, 700);
|
||||||
|
|
||||||
|
expect(result400).toBe('https://example.com/font-variable.woff2');
|
||||||
|
expect(result700).toBe('https://example.com/font-variable.woff2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fallback logic', () => {
|
||||||
|
it('falls back to regular when exact weight not found', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
regular: 'https://example.com/font-regular.woff2',
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 700);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-regular.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to variant 400 when exact weight and regular not found', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 700);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-400.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to variant regular when exact weight, regular, and 400 not found', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'700': 'https://example.com/font-700.woff2',
|
||||||
|
'regular': 'https://example.com/font-regular.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 400);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-regular.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers regular over variants.400 for fallback', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
regular: 'https://example.com/font-regular.woff2',
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 700);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-regular.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined when no fallback options available', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'700': 'https://example.com/font-700.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 400);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined for font with empty styles', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 400);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for font with undefined styles (invalid font data)', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: undefined as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getFontUrl(font, 400)).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('handles font with only regular URL (legacy format)', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
regular: 'https://example.com/font-regular.woff2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 700);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-regular.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles font with only variants object', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
'700': 'https://example.com/font-700.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result400 = getFontUrl(font, 400);
|
||||||
|
const result700 = getFontUrl(font, 700);
|
||||||
|
|
||||||
|
expect(result400).toBe('https://example.com/font-400.woff2');
|
||||||
|
expect(result700).toBe('https://example.com/font-700.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles font with variants but no requested weight', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 700);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-400.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Google Fonts style with legacy URLs', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
regular: 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2',
|
||||||
|
bold: 'https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1Mu72xWUlvAx05IsDqlA.woff2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 700);
|
||||||
|
|
||||||
|
expect(result).toBe('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Fontshare fonts with multiple weights', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'100': 'https://cdn.fontshare.com/wf/font-100.woff2',
|
||||||
|
'200': 'https://cdn.fontshare.com/wf/font-200.woff2',
|
||||||
|
'300': 'https://cdn.fontshare.com/wf/font-300.woff2',
|
||||||
|
'400': 'https://cdn.fontshare.com/wf/font-400.woff2',
|
||||||
|
'500': 'https://cdn.fontshare.com/wf/font-500.woff2',
|
||||||
|
'600': 'https://cdn.fontshare.com/wf/font-600.woff2',
|
||||||
|
'700': 'https://cdn.fontshare.com/wf/font-700.woff2',
|
||||||
|
'800': 'https://cdn.fontshare.com/wf/font-800.woff2',
|
||||||
|
'900': 'https://cdn.fontshare.com/wf/font-900.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test all valid weights
|
||||||
|
for (const weight of [100, 200, 300, 400, 500, 600, 700, 800, 900]) {
|
||||||
|
const result = getFontUrl(font, weight);
|
||||||
|
expect(result).toBe(`https://cdn.fontshare.com/wf/font-${weight}.woff2`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles font with partial weight coverage', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-regular.woff2',
|
||||||
|
'700': 'https://example.com/font-bold.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result400 = getFontUrl(font, 400);
|
||||||
|
const result700 = getFontUrl(font, 700);
|
||||||
|
const result500 = getFontUrl(font, 500);
|
||||||
|
|
||||||
|
expect(result400).toBe('https://example.com/font-regular.woff2');
|
||||||
|
expect(result700).toBe('https://example.com/font-bold.woff2');
|
||||||
|
expect(result500).toBe('https://example.com/font-regular.woff2'); // Fallback
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles font with variants.regular as fallback', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'700': 'https://example.com/font-bold.woff2',
|
||||||
|
'regular': 'https://example.com/font-regular.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 400);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-regular.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty variants object', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 400);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined when variant URL is null and no fallback available', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': null as any,
|
||||||
|
'700': 'https://example.com/font-bold.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 400);
|
||||||
|
|
||||||
|
// null is falsy, so it falls back to regular, 400, and then regular variant
|
||||||
|
// All are undefined, so returns undefined
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('boundary tests', () => {
|
||||||
|
it('handles lowest valid weight (100)', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'100': 'https://example.com/font-100.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 100);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-100.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles highest valid weight (900)', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'900': 'https://example.com/font-900.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 900);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-900.woff2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles middle weight (500)', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'500': 'https://example.com/font-500.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, 500);
|
||||||
|
|
||||||
|
expect(result).toBe('https://example.com/font-500.woff2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('invalid weights', () => {
|
||||||
|
it('throws error for weight below 100', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getFontUrl(font, 99)).toThrow('Invalid weight: 99');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for weight above 900', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getFontUrl(font, 901)).toThrow('Invalid weight: 901');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for weight 0', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getFontUrl(font, 0)).toThrow('Invalid weight: 0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for negative weight', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getFontUrl(font, -100)).toThrow('Invalid weight: -100');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for non-numeric weight', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore - Testing invalid input type
|
||||||
|
expect(() => getFontUrl(font, '400' as any)).toThrow('Invalid weight: 400');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for decimal weight', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getFontUrl(font, 450.5)).toThrow('Invalid weight: 450.5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for weight with step of 50 (not supported)', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getFontUrl(font, 450)).toThrow('Invalid weight: 450');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for weight with step of 10 (not supported)', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getFontUrl(font, 410)).toThrow('Invalid weight: 410');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for NaN weight', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getFontUrl(font, NaN)).toThrow('Invalid weight: NaN');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for Infinity weight', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getFontUrl(font, Infinity)).toThrow('Invalid weight: Infinity');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws descriptive error message', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://example.com/font-400.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
getFontUrl(font, 999);
|
||||||
|
expect.fail('Expected function to throw');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(Error);
|
||||||
|
expect((error as Error).message).toBe('Invalid weight: 999');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('provider-specific tests', () => {
|
||||||
|
it('handles Google Fonts with variable fonts', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
provider: 'google',
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2',
|
||||||
|
'700': 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKOzY.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result400 = getFontUrl(font, 400);
|
||||||
|
const result700 = getFontUrl(font, 700);
|
||||||
|
|
||||||
|
// Variable fonts return the same URL for all weights
|
||||||
|
expect(result400).toBe(result700);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Fontshare fonts with static weights', () => {
|
||||||
|
const font = createMockFont({
|
||||||
|
provider: 'fontshare',
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
'400': 'https://cdn.fontshare.com/wf/satoshi-regular.woff2',
|
||||||
|
'700': 'https://cdn.fontshare.com/wf/satoshi-bold.woff2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result400 = getFontUrl(font, 400);
|
||||||
|
const result700 = getFontUrl(font, 700);
|
||||||
|
|
||||||
|
expect(result400).toBe('https://cdn.fontshare.com/wf/satoshi-regular.woff2');
|
||||||
|
expect(result700).toBe('https://cdn.fontshare.com/wf/satoshi-bold.woff2');
|
||||||
|
expect(result400).not.toBe(result700);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('all valid weights test', () => {
|
||||||
|
it('handles all valid weight values', () => {
|
||||||
|
const validWeights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||||
|
|
||||||
|
validWeights.forEach(weight => {
|
||||||
|
const font = createMockFont({
|
||||||
|
styles: {
|
||||||
|
variants: {
|
||||||
|
[weight.toString()]: `https://example.com/font-${weight}.woff2`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = getFontUrl(font, weight);
|
||||||
|
expect(result).toBe(`https://example.com/font-${weight}.woff2`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
29
src/entities/Font/lib/getFontUrl/getFontUrl.ts
Normal file
29
src/entities/Font/lib/getFontUrl/getFontUrl.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type {
|
||||||
|
FontWeight,
|
||||||
|
UnifiedFont,
|
||||||
|
} from '../../model';
|
||||||
|
|
||||||
|
const SIZES = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a URL for a font based on the provided font and weight.
|
||||||
|
* @param font - The font object.
|
||||||
|
* @param weight - The weight of the font.
|
||||||
|
* @returns The URL for the font.
|
||||||
|
*/
|
||||||
|
export function getFontUrl(font: UnifiedFont, weight: number): string | undefined {
|
||||||
|
if (!SIZES.includes(weight)) {
|
||||||
|
throw new Error(`Invalid weight: ${weight}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const weightKey = weight.toString() as FontWeight;
|
||||||
|
|
||||||
|
// 1. Try exact match (Backend now maps "100".."900" to VF URL if variable)
|
||||||
|
if (font.styles.variants?.[weightKey]) {
|
||||||
|
return font.styles.variants[weightKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallbacks for Static Fonts (if exact weight missing)
|
||||||
|
// Try 'regular' or '400' as safe defaults
|
||||||
|
return font.styles.regular || font.styles.variants?.['400'] || font.styles.variants?.['regular'];
|
||||||
|
}
|
||||||
@@ -4,3 +4,5 @@ export {
|
|||||||
normalizeGoogleFont,
|
normalizeGoogleFont,
|
||||||
normalizeGoogleFonts,
|
normalizeGoogleFonts,
|
||||||
} from './normalize/normalize';
|
} from './normalize/normalize';
|
||||||
|
|
||||||
|
export { getFontUrl } from './getFontUrl/getFontUrl';
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
FontshareFont,
|
FontshareFont,
|
||||||
GoogleFontItem,
|
GoogleFontItem,
|
||||||
UnifiedFont,
|
UnifiedFont,
|
||||||
|
UnifiedFontVariant,
|
||||||
} from '../../model/types';
|
} from '../../model/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,7 +187,7 @@ export function normalizeFontshareFont(apiFont: FontshareFont): UnifiedFont {
|
|||||||
const variants = apiFont.styles.map(style => {
|
const variants = apiFont.styles.map(style => {
|
||||||
const weightLabel = style.weight.label;
|
const weightLabel = style.weight.label;
|
||||||
const isItalic = style.is_italic;
|
const isItalic = style.is_italic;
|
||||||
return isItalic ? `${weightLabel}italic` : weightLabel;
|
return (isItalic ? `${weightLabel}italic` : weightLabel) as UnifiedFontVariant;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map styles to URLs
|
// Map styles to URLs
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export type {
|
|||||||
export {
|
export {
|
||||||
appliedFontsManager,
|
appliedFontsManager,
|
||||||
createUnifiedFontStore,
|
createUnifiedFontStore,
|
||||||
|
type FontConfigRequest,
|
||||||
selectedFontsStore,
|
selectedFontsStore,
|
||||||
type UnifiedFontStore,
|
type UnifiedFontStore,
|
||||||
unifiedFontStore,
|
unifiedFontStore,
|
||||||
|
|||||||
@@ -52,16 +52,27 @@ class AppliedFontsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#getFontKey(id: string, weight: number): string {
|
#getFontKey(config: FontConfigRequest): string {
|
||||||
return `${id.toLowerCase()}@${weight}`;
|
if (config.isVariable) {
|
||||||
|
// For variable fonts, the ID is unique enough.
|
||||||
|
// Loading "Roboto" once covers "Roboto 400" and "Roboto 700"
|
||||||
|
return `${config.id.toLowerCase()}@vf`;
|
||||||
|
}
|
||||||
|
// For static fonts, we still need weight separation
|
||||||
|
return `${config.id.toLowerCase()}@${config.weight}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
touch(configs: FontConfigRequest[]) {
|
touch(configs: FontConfigRequest[]) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
configs.forEach(config => {
|
configs.forEach(config => {
|
||||||
const key = this.#getFontKey(config.id, config.weight);
|
// Pass the whole config to get key
|
||||||
|
const key = this.#getFontKey(config);
|
||||||
|
|
||||||
this.#usageTracker.set(key, now);
|
this.#usageTracker.set(key, now);
|
||||||
|
|
||||||
|
// If it's already loaded, we don't need to do anything
|
||||||
|
if (this.statuses.get(key) === 'loaded') return;
|
||||||
|
|
||||||
if (!this.#idToBatch.has(key) && !this.#queue.has(key)) {
|
if (!this.#idToBatch.has(key) && !this.#queue.has(key)) {
|
||||||
this.#queue.set(key, config);
|
this.#queue.set(key, config);
|
||||||
|
|
||||||
@@ -71,8 +82,10 @@ class AppliedFontsManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getFontStatus(id: string, weight: number) {
|
getFontStatus(id: string, weight: number, isVariable: boolean = false) {
|
||||||
return this.statuses.get(this.#getFontKey(id, weight));
|
// Construct a temp config to generate key
|
||||||
|
const key = this.#getFontKey({ id, weight, name: '', url: '', isVariable });
|
||||||
|
return this.statuses.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
#processQueue() {
|
#processQueue() {
|
||||||
@@ -97,27 +110,31 @@ class AppliedFontsManager {
|
|||||||
this.statuses.set(key, 'loading');
|
this.statuses.set(key, 'loading');
|
||||||
this.#idToBatch.set(key, batchId);
|
this.#idToBatch.set(key, batchId);
|
||||||
|
|
||||||
// Construct the @font-face rule
|
// If variable, allow the full weight range.
|
||||||
// Using format('truetype') for .ttf
|
// If static, lock it to the specific weight.
|
||||||
|
const weightRule = config.isVariable
|
||||||
|
? '100 900' // Variable range (standard coverage)
|
||||||
|
: config.weight;
|
||||||
|
const fontFormat = config.isVariable ? 'truetype-variations' : 'truetype';
|
||||||
|
|
||||||
cssRules += `
|
cssRules += `
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: '${config.name}';
|
font-family: '${config.name}';
|
||||||
src: url('${config.url}') format('truetype');
|
src: url('${config.url}') format('${fontFormat}');
|
||||||
font-weight: ${config.weight};
|
font-weight: ${weightRule};
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create and inject the style tag
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.dataset.batchId = batchId;
|
style.dataset.batchId = batchId;
|
||||||
style.innerHTML = cssRules;
|
style.innerHTML = cssRules;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
this.#batchElements.set(batchId, style);
|
this.#batchElements.set(batchId, style);
|
||||||
|
|
||||||
// Verify loading via Font Loading API
|
// Use the requested weight for verification, even if the rule covers a range
|
||||||
batchEntries.forEach(([key, config]) => {
|
batchEntries.forEach(([key, config]) => {
|
||||||
document.fonts.load(`${config.weight} 1em "${config.name}"`)
|
document.fonts.load(`${config.weight} 1em "${config.name}"`)
|
||||||
.then(loaded => {
|
.then(loaded => {
|
||||||
@@ -126,7 +143,6 @@ class AppliedFontsManager {
|
|||||||
.catch(() => this.statuses.set(key, 'error'));
|
.catch(() => this.statuses.set(key, 'error'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#purgeUnused() {
|
#purgeUnused() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const batchesToRemove = new Set<string>();
|
const batchesToRemove = new Set<string>();
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type { UnifiedFont } from '../types';
|
|||||||
|
|
||||||
/** */
|
/** */
|
||||||
export abstract class BaseFontStore<TParams extends Record<string, any>> {
|
export abstract class BaseFontStore<TParams extends Record<string, any>> {
|
||||||
// params = $state<TParams>({} as TParams);
|
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
|
|
||||||
#bindings = $state<(() => Partial<TParams>)[]>([]);
|
#bindings = $state<(() => Partial<TParams>)[]>([]);
|
||||||
@@ -18,9 +17,11 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
|
|||||||
params = $derived.by(() => {
|
params = $derived.by(() => {
|
||||||
let merged = { ...this.#internalParams };
|
let merged = { ...this.#internalParams };
|
||||||
|
|
||||||
|
// Loop through every "Cable" plugged into the store
|
||||||
// Loop through every "Cable" plugged into the store
|
// Loop through every "Cable" plugged into the store
|
||||||
for (const getter of this.#bindings) {
|
for (const getter of this.#bindings) {
|
||||||
merged = { ...merged, ...getter() };
|
const bindingResult = getter();
|
||||||
|
merged = { ...merged, ...bindingResult };
|
||||||
}
|
}
|
||||||
|
|
||||||
return merged as TParams;
|
return merged as TParams;
|
||||||
@@ -54,7 +55,7 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
|
|||||||
protected abstract getQueryKey(params: TParams): QueryKey;
|
protected abstract getQueryKey(params: TParams): QueryKey;
|
||||||
protected abstract fetchFn(params: TParams): Promise<UnifiedFont[]>;
|
protected abstract fetchFn(params: TParams): Promise<UnifiedFont[]>;
|
||||||
|
|
||||||
private getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
|
protected getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
|
||||||
return {
|
return {
|
||||||
queryKey: this.getQueryKey(params),
|
queryKey: this.getQueryKey(params),
|
||||||
queryFn: () => this.fetchFn(params),
|
queryFn: () => this.fetchFn(params),
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ export {
|
|||||||
} from './unifiedFontStore.svelte';
|
} from './unifiedFontStore.svelte';
|
||||||
|
|
||||||
// Applied fonts manager (CSS loading - unchanged)
|
// Applied fonts manager (CSS loading - unchanged)
|
||||||
export { appliedFontsManager } from './appliedFontsStore/appliedFontsStore.svelte';
|
export {
|
||||||
|
appliedFontsManager,
|
||||||
|
type FontConfigRequest,
|
||||||
|
} from './appliedFontsStore/appliedFontsStore.svelte';
|
||||||
|
|
||||||
// Selected fonts store (user selection - unchanged)
|
// Selected fonts store (user selection - unchanged)
|
||||||
export { selectedFontsStore } from './selectedFontsStore/selectedFontsStore.svelte';
|
export { selectedFontsStore } from './selectedFontsStore/selectedFontsStore.svelte';
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
* - Provider-specific shortcuts for common operations
|
* - Provider-specific shortcuts for common operations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { QueryObserverOptions } from '@tanstack/query-core';
|
||||||
import type { ProxyFontsParams } from '../../api';
|
import type { ProxyFontsParams } from '../../api';
|
||||||
import { fetchProxyFonts } from '../../api';
|
import { fetchProxyFonts } from '../../api';
|
||||||
import type { UnifiedFont } from '../types';
|
import type { UnifiedFont } from '../types';
|
||||||
@@ -121,6 +122,19 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
this.#previousFilterParams = filterParams;
|
this.#previousFilterParams = filterParams;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Effect: Sync state from Query result (Handles Cache Hits)
|
||||||
|
$effect(() => {
|
||||||
|
const data = this.result.data;
|
||||||
|
const offset = this.params.offset || 0;
|
||||||
|
|
||||||
|
// When we have data and we are at the start (offset 0),
|
||||||
|
// we must ensure accumulatedFonts matches the fresh (or cached) data.
|
||||||
|
// This fixes the issue where cache hits skip fetchFn side-effects.
|
||||||
|
if (offset === 0 && data && data.length > 0) {
|
||||||
|
this.#accumulatedFonts = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,15 +159,26 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
protected getQueryKey(params: ProxyFontsParams) {
|
protected getQueryKey(params: ProxyFontsParams) {
|
||||||
// Normalize params to treat empty arrays/strings as undefined
|
// Normalize params to treat empty arrays/strings as undefined
|
||||||
const normalized = Object.entries(params).reduce((acc, [key, value]) => {
|
const normalized = Object.entries(params).reduce((acc, [key, value]) => {
|
||||||
if (value === '' || (Array.isArray(value) && value.length === 0)) {
|
if (value === '' || value === undefined || (Array.isArray(value) && value.length === 0)) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
return { ...acc, [key]: value };
|
return { ...acc, [key]: value };
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
// Return a consistent key
|
||||||
return ['unifiedFonts', normalized] as const;
|
return ['unifiedFonts', normalized] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
|
||||||
|
const hasFilters = !!(params.q || params.provider || params.category || params.subset);
|
||||||
|
return {
|
||||||
|
queryKey: this.getQueryKey(params),
|
||||||
|
queryFn: () => this.fetchFn(params),
|
||||||
|
staleTime: hasFilters ? 0 : 5 * 60 * 1000,
|
||||||
|
gcTime: 10 * 60 * 1000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch function that calls the proxy API
|
* Fetch function that calls the proxy API
|
||||||
* Returns the full response including pagination metadata
|
* Returns the full response including pagination metadata
|
||||||
@@ -187,11 +212,10 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Accumulate fonts for infinite scroll
|
// Accumulate fonts for infinite scroll
|
||||||
if (params.offset === 0) {
|
// Note: For offset === 0, we rely on the $effect above to handle the reset/init
|
||||||
// Reset when starting from beginning (new search/filter)
|
// This prevents race conditions and double-setting.
|
||||||
this.#accumulatedFonts = response.fonts;
|
if (params.offset !== 0) {
|
||||||
} else {
|
// Append new fonts to existing ones only for pagination
|
||||||
// Append new fonts to existing ones
|
|
||||||
this.#accumulatedFonts = [...this.#accumulatedFonts, ...response.fonts];
|
this.#accumulatedFonts = [...this.#accumulatedFonts, ...response.fonts];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,3 +32,27 @@ export interface FontFilters {
|
|||||||
|
|
||||||
export type CheckboxFilter = 'providers' | 'categories' | 'subsets';
|
export type CheckboxFilter = 'providers' | 'categories' | 'subsets';
|
||||||
export type FilterType = CheckboxFilter | 'searchQuery';
|
export type FilterType = CheckboxFilter | 'searchQuery';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard font weights
|
||||||
|
*/
|
||||||
|
export type FontWeight = '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Italic variant format: e.g., "100italic", "400italic", "700italic"
|
||||||
|
*/
|
||||||
|
export type FontWeightItalic = `${FontWeight}italic`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All possible font variants
|
||||||
|
* - Numeric weights: "400", "700", etc.
|
||||||
|
* - Italic variants: "400italic", "700italic", etc.
|
||||||
|
* - Legacy names: "regular", "italic", "bold", "bolditalic"
|
||||||
|
*/
|
||||||
|
export type FontVariant =
|
||||||
|
| FontWeight
|
||||||
|
| FontWeightItalic
|
||||||
|
| 'regular'
|
||||||
|
| 'italic'
|
||||||
|
| 'bold'
|
||||||
|
| 'bolditalic';
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
* ============================================================================
|
* ============================================================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { FontVariant } from './common';
|
||||||
|
|
||||||
export type FontCategory = 'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace';
|
export type FontCategory = 'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,30 +88,6 @@ export interface FontItem {
|
|||||||
*/
|
*/
|
||||||
export type GoogleFontItem = FontItem;
|
export type GoogleFontItem = FontItem;
|
||||||
|
|
||||||
/**
|
|
||||||
* Standard font weights that can appear in Google Fonts API
|
|
||||||
*/
|
|
||||||
export type FontWeight = '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Italic variant format: e.g., "100italic", "400italic", "700italic"
|
|
||||||
*/
|
|
||||||
export type FontWeightItalic = `${FontWeight}italic`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All possible font variants in Google Fonts API
|
|
||||||
* - Numeric weights: "400", "700", etc.
|
|
||||||
* - Italic variants: "400italic", "700italic", etc.
|
|
||||||
* - Legacy names: "regular", "italic", "bold", "bolditalic"
|
|
||||||
*/
|
|
||||||
export type FontVariant =
|
|
||||||
| FontWeight
|
|
||||||
| FontWeightItalic
|
|
||||||
| 'regular'
|
|
||||||
| 'italic'
|
|
||||||
| 'bold'
|
|
||||||
| 'bolditalic';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Google Fonts API file mapping
|
* Google Fonts API file mapping
|
||||||
* Dynamic keys that match the variants array
|
* Dynamic keys that match the variants array
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ export type {
|
|||||||
FontCategory,
|
FontCategory,
|
||||||
FontProvider,
|
FontProvider,
|
||||||
FontSubset,
|
FontSubset,
|
||||||
|
FontVariant,
|
||||||
|
FontWeight,
|
||||||
|
FontWeightItalic,
|
||||||
} from './common';
|
} from './common';
|
||||||
|
|
||||||
// Google Fonts API types
|
// Google Fonts API types
|
||||||
export type {
|
export type {
|
||||||
FontFiles,
|
FontFiles,
|
||||||
FontItem,
|
FontItem,
|
||||||
FontVariant,
|
|
||||||
FontWeight,
|
|
||||||
FontWeightItalic,
|
|
||||||
GoogleFontItem,
|
GoogleFontItem,
|
||||||
GoogleFontsApiModel,
|
GoogleFontsApiModel,
|
||||||
} from './google';
|
} from './google';
|
||||||
|
|||||||
@@ -8,17 +8,18 @@ import type {
|
|||||||
FontCategory,
|
FontCategory,
|
||||||
FontProvider,
|
FontProvider,
|
||||||
FontSubset,
|
FontSubset,
|
||||||
|
FontVariant,
|
||||||
} from './common';
|
} from './common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Font variant types (standardized)
|
* Font variant types (standardized)
|
||||||
*/
|
*/
|
||||||
export type UnifiedFontVariant = string;
|
export type UnifiedFontVariant = FontVariant;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Font style URLs
|
* Font style URLs
|
||||||
*/
|
*/
|
||||||
export interface FontStyleUrls {
|
export interface LegacyFontStyleUrls {
|
||||||
/** Regular weight URL */
|
/** Regular weight URL */
|
||||||
regular?: string;
|
regular?: string;
|
||||||
/** Italic URL */
|
/** Italic URL */
|
||||||
@@ -29,6 +30,10 @@ export interface FontStyleUrls {
|
|||||||
boldItalic?: string;
|
boldItalic?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FontStyleUrls extends LegacyFontStyleUrls {
|
||||||
|
variants?: Partial<Record<UnifiedFontVariant, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Font metadata
|
* Font metadata
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ interface Props {
|
|||||||
* Font weight
|
* Font weight
|
||||||
*/
|
*/
|
||||||
weight?: number;
|
weight?: number;
|
||||||
|
|
||||||
|
isVariable?: boolean;
|
||||||
/**
|
/**
|
||||||
* Additional classes
|
* Additional classes
|
||||||
*/
|
*/
|
||||||
@@ -36,27 +38,42 @@ interface Props {
|
|||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { name, id, url, weight = 400, className, children }: Props = $props();
|
let { name, id, url, weight = 400, isVariable = false, className, children }: Props = $props();
|
||||||
let element: Element;
|
let element: Element;
|
||||||
|
|
||||||
// Track if the user has actually scrolled this into view
|
// Track if the user has actually scrolled this into view
|
||||||
let hasEnteredViewport = $state(false);
|
let hasEnteredViewport = $state(false);
|
||||||
|
const status = $derived(appliedFontsManager.getFontStatus(id, weight, isVariable));
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
if (status === 'loaded' || status === 'error') {
|
||||||
|
hasEnteredViewport = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const observer = new IntersectionObserver(entries => {
|
const observer = new IntersectionObserver(entries => {
|
||||||
if (entries[0].isIntersecting) {
|
if (entries[0].isIntersecting) {
|
||||||
hasEnteredViewport = true;
|
hasEnteredViewport = true;
|
||||||
appliedFontsManager.touch([{ id, weight, name, url }]);
|
|
||||||
|
|
||||||
// Once it has entered, we can stop observing to save CPU
|
// Touch ensures it's in the queue.
|
||||||
|
// It's safe to call this even if VirtualList called it
|
||||||
|
// (Manager dedupes based on key)
|
||||||
|
appliedFontsManager.touch([{
|
||||||
|
id,
|
||||||
|
weight,
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
isVariable,
|
||||||
|
}]);
|
||||||
|
|
||||||
observer.unobserve(element);
|
observer.unobserve(element);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
observer.observe(element);
|
|
||||||
|
if (element) observer.observe(element);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
const status = $derived(appliedFontsManager.getFontStatus(id, weight));
|
|
||||||
// The "Show" condition: Element is in view AND (Font is ready OR it errored out)
|
// The "Show" condition: Element is in view AND (Font is ready OR it errored out)
|
||||||
const shouldReveal = $derived(hasEnteredViewport && (status === 'loaded' || status === 'error'));
|
const shouldReveal = $derived(hasEnteredViewport && (status === 'loaded' || status === 'error'));
|
||||||
|
|
||||||
@@ -69,7 +86,7 @@ const transitionClasses = $derived(
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
style:font-family={name}
|
style:font-family={shouldReveal ? `'${name}'` : 'sans-serif'}
|
||||||
class={cn(
|
class={cn(
|
||||||
transitionClasses,
|
transitionClasses,
|
||||||
// If reduced motion is on, we skip the transform/blur entirely
|
// If reduced motion is on, we skip the transform/blur entirely
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
- Handles font registration with the manager
|
- Handles font registration with the manager
|
||||||
-->
|
-->
|
||||||
<script lang="ts" generics="T extends UnifiedFont">
|
<script lang="ts" generics="T extends UnifiedFont">
|
||||||
import type { FontConfigRequest } from '$entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte';
|
|
||||||
import { VirtualList } from '$shared/ui';
|
import { VirtualList } from '$shared/ui';
|
||||||
import type { ComponentProps } from 'svelte';
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import { getFontUrl } from '../../lib';
|
||||||
|
import type { FontConfigRequest } from '../../model';
|
||||||
import {
|
import {
|
||||||
type UnifiedFont,
|
type UnifiedFont,
|
||||||
appliedFontsManager,
|
appliedFontsManager,
|
||||||
@@ -21,16 +22,25 @@ interface Props extends Omit<ComponentProps<typeof VirtualList<T>>, 'onVisibleIt
|
|||||||
let { items, children, onVisibleItemsChange, onNearBottom, weight, ...rest }: Props = $props();
|
let { items, children, onVisibleItemsChange, onNearBottom, weight, ...rest }: Props = $props();
|
||||||
|
|
||||||
function handleInternalVisibleChange(visibleItems: T[]) {
|
function handleInternalVisibleChange(visibleItems: T[]) {
|
||||||
|
const configs: FontConfigRequest[] = [];
|
||||||
|
|
||||||
|
visibleItems.forEach(item => {
|
||||||
|
const url = getFontUrl(item, weight);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
configs.push({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
weight,
|
||||||
|
url,
|
||||||
|
isVariable: item.features?.isVariable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
// Auto-register fonts with the manager
|
// Auto-register fonts with the manager
|
||||||
const configs = visibleItems.map<FontConfigRequest>(item => ({
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
weight,
|
|
||||||
url: item.styles.regular!,
|
|
||||||
}));
|
|
||||||
appliedFontsManager.touch(configs);
|
appliedFontsManager.touch(configs);
|
||||||
|
|
||||||
// // Forward the call to any external listener
|
// Forward the call to any external listener
|
||||||
// onVisibleItemsChange?.(visibleItems);
|
// onVisibleItemsChange?.(visibleItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user