Add comprehensive library expansion with new components and demos
- Add new libraries: ui-accessibility, ui-animations, ui-backgrounds, ui-code-display, ui-data-utils, ui-font-manager, hcl-studio - Add extensive layout components: gallery-grid, infinite-scroll-container, kanban-board, masonry, split-view, sticky-layout - Add comprehensive demo components for all new features - Update project configuration and dependencies - Expand component exports and routing structure - Add UI landing pages planning document 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
240
projects/ui-font-manager/src/lib/presets/font-preset-manager.ts
Normal file
240
projects/ui-font-manager/src/lib/presets/font-preset-manager.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { FontDefinition, FontPreset } from '../types/font-types';
|
||||
|
||||
export class FontPresetManager {
|
||||
private customPresets = new Map<string, FontPreset>();
|
||||
private fontCategories = new Map<string, string[]>();
|
||||
|
||||
constructor() {
|
||||
this.initializeCategories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom font preset collection
|
||||
*/
|
||||
addPresetCollection(name: string, preset: FontPreset): void {
|
||||
this.customPresets.set(name, preset);
|
||||
|
||||
// Update categories
|
||||
Object.entries(preset).forEach(([fontName, definition]) => {
|
||||
this.addToCategory(definition.category, fontName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single custom font
|
||||
*/
|
||||
addCustomFont(name: string, definition: FontDefinition): void {
|
||||
const customPreset = this.customPresets.get('custom') || {};
|
||||
customPreset[name] = definition;
|
||||
this.customPresets.set('custom', customPreset);
|
||||
|
||||
this.addToCategory(definition.category, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add font from external service (Typekit, custom CDN, etc.)
|
||||
*/
|
||||
addExternalFont(name: string, config: {
|
||||
family: string;
|
||||
url: string;
|
||||
weights?: number[];
|
||||
category?: FontDefinition['category'];
|
||||
fallbacks?: string[];
|
||||
}): void {
|
||||
const definition: FontDefinition = {
|
||||
family: config.family,
|
||||
url: config.url,
|
||||
weights: config.weights || [400],
|
||||
fallbacks: config.fallbacks || this.getDefaultFallbacks(config.category || 'sans-serif'),
|
||||
loadingStrategy: 'swap',
|
||||
category: config.category || 'sans-serif'
|
||||
};
|
||||
|
||||
this.addCustomFont(name, definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Adobe Typekit font
|
||||
*/
|
||||
addTypekitFont(kitId: string, fonts: Array<{
|
||||
name: string;
|
||||
family: string;
|
||||
weights?: number[];
|
||||
category?: FontDefinition['category'];
|
||||
}>): void {
|
||||
const typekitUrl = `https://use.typekit.net/${kitId}.css`;
|
||||
|
||||
fonts.forEach(font => {
|
||||
this.addExternalFont(font.name, {
|
||||
family: font.family,
|
||||
url: typekitUrl,
|
||||
weights: font.weights,
|
||||
category: font.category
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add self-hosted font
|
||||
*/
|
||||
addSelfHostedFont(name: string, config: {
|
||||
family: string;
|
||||
basePath: string; // e.g., '/assets/fonts/my-font'
|
||||
formats: Array<'woff2' | 'woff' | 'ttf' | 'otf'>;
|
||||
weights?: number[];
|
||||
category?: FontDefinition['category'];
|
||||
fallbacks?: string[];
|
||||
}): void {
|
||||
// Generate CSS for self-hosted font
|
||||
const fontFaceCSS = this.generateFontFaceCSS(config);
|
||||
|
||||
const definition: FontDefinition = {
|
||||
family: config.family,
|
||||
url: `data:text/css;base64,${btoa(fontFaceCSS)}`, // Inline CSS
|
||||
weights: config.weights || [400],
|
||||
fallbacks: config.fallbacks || this.getDefaultFallbacks(config.category || 'sans-serif'),
|
||||
loadingStrategy: 'swap',
|
||||
category: config.category || 'sans-serif'
|
||||
};
|
||||
|
||||
this.addCustomFont(name, definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all custom presets
|
||||
*/
|
||||
getCustomPresets(): Map<string, FontPreset> {
|
||||
return this.customPresets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fonts by category
|
||||
*/
|
||||
getFontsByCategory(category: string): string[] {
|
||||
return this.fontCategories.get(category) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories
|
||||
*/
|
||||
getCategories(): string[] {
|
||||
return Array.from(this.fontCategories.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove custom font
|
||||
*/
|
||||
removeCustomFont(name: string): boolean {
|
||||
const customPreset = this.customPresets.get('custom');
|
||||
if (customPreset && customPreset[name]) {
|
||||
const category = customPreset[name].category;
|
||||
delete customPreset[name];
|
||||
|
||||
// Remove from category
|
||||
this.removeFromCategory(category, name);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import fonts from a configuration object
|
||||
*/
|
||||
importConfiguration(config: {
|
||||
name: string;
|
||||
fonts: { [key: string]: Omit<FontDefinition, 'loadingStrategy'> };
|
||||
}): void {
|
||||
const preset: FontPreset = {};
|
||||
|
||||
Object.entries(config.fonts).forEach(([name, fontConfig]) => {
|
||||
preset[name] = {
|
||||
...fontConfig,
|
||||
loadingStrategy: 'swap'
|
||||
};
|
||||
});
|
||||
|
||||
this.addPresetCollection(config.name, preset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export custom fonts configuration
|
||||
*/
|
||||
exportConfiguration(): string {
|
||||
const config = {
|
||||
customPresets: Object.fromEntries(this.customPresets),
|
||||
categories: Object.fromEntries(this.fontCategories)
|
||||
};
|
||||
|
||||
return JSON.stringify(config, null, 2);
|
||||
}
|
||||
|
||||
private initializeCategories(): void {
|
||||
this.fontCategories.set('sans-serif', []);
|
||||
this.fontCategories.set('serif', []);
|
||||
this.fontCategories.set('monospace', []);
|
||||
this.fontCategories.set('display', []);
|
||||
this.fontCategories.set('handwriting', []);
|
||||
}
|
||||
|
||||
private addToCategory(category: string, fontName: string): void {
|
||||
const fonts = this.fontCategories.get(category) || [];
|
||||
if (!fonts.includes(fontName)) {
|
||||
fonts.push(fontName);
|
||||
this.fontCategories.set(category, fonts);
|
||||
}
|
||||
}
|
||||
|
||||
private removeFromCategory(category: string, fontName: string): void {
|
||||
const fonts = this.fontCategories.get(category) || [];
|
||||
const index = fonts.indexOf(fontName);
|
||||
if (index > -1) {
|
||||
fonts.splice(index, 1);
|
||||
this.fontCategories.set(category, fonts);
|
||||
}
|
||||
}
|
||||
|
||||
private getDefaultFallbacks(category: FontDefinition['category']): string[] {
|
||||
switch (category) {
|
||||
case 'serif':
|
||||
return ['Georgia', 'Cambria', 'Times New Roman', 'Times', 'serif'];
|
||||
case 'monospace':
|
||||
return ['Consolas', 'Monaco', 'Courier New', 'monospace'];
|
||||
case 'handwriting':
|
||||
return ['cursive'];
|
||||
case 'display':
|
||||
case 'sans-serif':
|
||||
default:
|
||||
return ['ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'];
|
||||
}
|
||||
}
|
||||
|
||||
private generateFontFaceCSS(config: {
|
||||
family: string;
|
||||
basePath: string;
|
||||
formats: Array<'woff2' | 'woff' | 'ttf' | 'otf'>;
|
||||
weights?: number[];
|
||||
}): string {
|
||||
const weights = config.weights || [400];
|
||||
let css = '';
|
||||
|
||||
weights.forEach(weight => {
|
||||
css += `@font-face {
|
||||
font-family: '${config.family}';
|
||||
src: ${config.formats.map(format => {
|
||||
const ext = format;
|
||||
const type = format === 'ttf' ? 'truetype' : format === 'otf' ? 'opentype' : format;
|
||||
return `url('${config.basePath}-${weight}.${ext}') format('${type}')`;
|
||||
}).join(',\n ')};
|
||||
font-weight: ${weight};
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
`;
|
||||
});
|
||||
|
||||
return css;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const fontPresetManager = new FontPresetManager();
|
||||
806
projects/ui-font-manager/src/lib/presets/google-fonts-preset.ts
Normal file
806
projects/ui-font-manager/src/lib/presets/google-fonts-preset.ts
Normal file
@@ -0,0 +1,806 @@
|
||||
import { FontPreset } from '../types/font-types';
|
||||
|
||||
export const GOOGLE_FONTS_PRESET: FontPreset = {
|
||||
// Sans-serif fonts (35 fonts)
|
||||
'Inter': {
|
||||
family: 'Inter',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 95
|
||||
},
|
||||
'Roboto': {
|
||||
family: 'Roboto',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900',
|
||||
weights: [100, 300, 400, 500, 700, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 98
|
||||
},
|
||||
'Open Sans': {
|
||||
family: 'Open Sans',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800',
|
||||
weights: [300, 400, 500, 600, 700, 800],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 92
|
||||
},
|
||||
'Poppins': {
|
||||
family: 'Poppins',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 85
|
||||
},
|
||||
'Lato': {
|
||||
family: 'Lato',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900',
|
||||
weights: [100, 300, 400, 700, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 80
|
||||
},
|
||||
'Montserrat': {
|
||||
family: 'Montserrat',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 88
|
||||
},
|
||||
'Nunito': {
|
||||
family: 'Nunito',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000',
|
||||
weights: [200, 300, 400, 500, 600, 700, 800, 900, 1000],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 75
|
||||
},
|
||||
'Source Sans Pro': {
|
||||
family: 'Source Sans Pro',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700;1,900',
|
||||
weights: [200, 300, 400, 600, 700, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 70
|
||||
},
|
||||
'Raleway': {
|
||||
family: 'Raleway',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 75
|
||||
},
|
||||
'Ubuntu': {
|
||||
family: 'Ubuntu',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700',
|
||||
weights: [300, 400, 500, 700],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 68
|
||||
},
|
||||
'Work Sans': {
|
||||
family: 'Work Sans',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100..900;1,100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 65
|
||||
},
|
||||
'Rubik': {
|
||||
family: 'Rubik',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300..900;1,300..900',
|
||||
weights: [300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 72
|
||||
},
|
||||
'Kanit': {
|
||||
family: 'Kanit',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Kanit:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 62
|
||||
},
|
||||
'DM Sans': {
|
||||
family: 'DM Sans',
|
||||
url: 'https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000',
|
||||
weights: [400, 500, 700],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 58
|
||||
},
|
||||
'Mulish': {
|
||||
family: 'Mulish',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Mulish:ital,wght@0,200..1000;1,200..1000',
|
||||
weights: [200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 55
|
||||
},
|
||||
'Hind': {
|
||||
family: 'Hind',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Hind:wght@300;400;500;600;700',
|
||||
weights: [300, 400, 500, 600, 700],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 52
|
||||
},
|
||||
'Quicksand': {
|
||||
family: 'Quicksand',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Quicksand:wght@300..700',
|
||||
weights: [300, 400, 500, 600, 700],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 60
|
||||
},
|
||||
'Karla': {
|
||||
family: 'Karla',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Karla:ital,wght@0,200..800;1,200..800',
|
||||
weights: [200, 300, 400, 500, 600, 700, 800],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 48
|
||||
},
|
||||
'Manrope': {
|
||||
family: 'Manrope',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Manrope:wght@200..800',
|
||||
weights: [200, 300, 400, 500, 600, 700, 800],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 45
|
||||
},
|
||||
'Comfortaa': {
|
||||
family: 'Comfortaa',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Comfortaa:wght@300..700',
|
||||
weights: [300, 400, 500, 600, 700],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 42
|
||||
},
|
||||
'Outfit': {
|
||||
family: 'Outfit',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Outfit:wght@100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 38
|
||||
},
|
||||
'Lexend': {
|
||||
family: 'Lexend',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Lexend:wght@100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 35
|
||||
},
|
||||
'Plus Jakarta Sans': {
|
||||
family: 'Plus Jakarta Sans',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800',
|
||||
weights: [200, 300, 400, 500, 600, 700, 800],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 32
|
||||
},
|
||||
'Space Grotesk': {
|
||||
family: 'Space Grotesk',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700',
|
||||
weights: [300, 400, 500, 600, 700],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 30
|
||||
},
|
||||
'Figtree': {
|
||||
family: 'Figtree',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,300..900;1,300..900',
|
||||
weights: [300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 28
|
||||
},
|
||||
'Barlow': {
|
||||
family: 'Barlow',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 25
|
||||
},
|
||||
'Epilogue': {
|
||||
family: 'Epilogue',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Epilogue:ital,wght@0,100..900;1,100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 22
|
||||
},
|
||||
'Sora': {
|
||||
family: 'Sora',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Sora:wght@100..800',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 20
|
||||
},
|
||||
'Red Hat Display': {
|
||||
family: 'Red Hat Display',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Red+Hat+Display:ital,wght@0,300..900;1,300..900',
|
||||
weights: [300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 18
|
||||
},
|
||||
'Satoshi': {
|
||||
family: 'Satoshi',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Satoshi:ital,wght@0,300;0,400;0,500;0,700;0,900;1,300;1,400;1,500;1,700;1,900',
|
||||
weights: [300, 400, 500, 700, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 15
|
||||
},
|
||||
'Heebo': {
|
||||
family: 'Heebo',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Heebo:wght@100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 12
|
||||
},
|
||||
'Exo 2': {
|
||||
family: 'Exo 2',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100..900;1,100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 10
|
||||
},
|
||||
'Commissioner': {
|
||||
family: 'Commissioner',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Commissioner:wght@100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 8
|
||||
},
|
||||
'Archivo': {
|
||||
family: 'Archivo',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,100..900;1,100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 6
|
||||
},
|
||||
'Public Sans': {
|
||||
family: 'Public Sans',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,100..900;1,100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif',
|
||||
popularity: 4
|
||||
},
|
||||
|
||||
// Serif fonts (20 fonts)
|
||||
'Playfair Display': {
|
||||
family: 'Playfair Display',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900',
|
||||
weights: [400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 85
|
||||
},
|
||||
'Merriweather': {
|
||||
family: 'Merriweather',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700;1,900',
|
||||
weights: [300, 400, 700, 900],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 78
|
||||
},
|
||||
'Lora': {
|
||||
family: 'Lora',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700',
|
||||
weights: [400, 500, 600, 700],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 72
|
||||
},
|
||||
'EB Garamond': {
|
||||
family: 'EB Garamond',
|
||||
url: 'https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400..800;1,400..800',
|
||||
weights: [400, 500, 600, 700, 800],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 65
|
||||
},
|
||||
'Crimson Text': {
|
||||
family: 'Crimson Text',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700',
|
||||
weights: [400, 600, 700],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 60
|
||||
},
|
||||
'Source Serif Pro': {
|
||||
family: 'Source Serif Pro',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,200..900;1,200..900',
|
||||
weights: [200, 300, 400, 600, 700, 900],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 55
|
||||
},
|
||||
'Noto Serif': {
|
||||
family: 'Noto Serif',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Noto+Serif:ital,wght@0,100..900;1,100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 50
|
||||
},
|
||||
'Vollkorn': {
|
||||
family: 'Vollkorn',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Vollkorn:ital,wght@0,400..900;1,400..900',
|
||||
weights: [400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 45
|
||||
},
|
||||
'Libre Baskerville': {
|
||||
family: 'Libre Baskerville',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 42
|
||||
},
|
||||
'Arvo': {
|
||||
family: 'Arvo',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Arvo:ital,wght@0,400;0,700;1,400;1,700',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 38
|
||||
},
|
||||
'Zilla Slab': {
|
||||
family: 'Zilla Slab',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Zilla+Slab:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700',
|
||||
weights: [300, 400, 500, 600, 700],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 35
|
||||
},
|
||||
'Cormorant Garamond': {
|
||||
family: 'Cormorant Garamond',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700',
|
||||
weights: [300, 400, 500, 600, 700],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 32
|
||||
},
|
||||
'PT Serif': {
|
||||
family: 'PT Serif',
|
||||
url: 'https://fonts.googleapis.com/css2?family=PT+Serif:ital,wght@0,400;0,700;1,400;1,700',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 30
|
||||
},
|
||||
'Alegreya': {
|
||||
family: 'Alegreya',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Alegreya:ital,wght@0,400..900;1,400..900',
|
||||
weights: [400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 28
|
||||
},
|
||||
'Spectral': {
|
||||
family: 'Spectral',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800',
|
||||
weights: [200, 300, 400, 500, 600, 700, 800],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 25
|
||||
},
|
||||
'Old Standard TT': {
|
||||
family: 'Old Standard TT',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Old+Standard+TT:ital,wght@0,400;0,700;1,400',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 22
|
||||
},
|
||||
'Cardo': {
|
||||
family: 'Cardo',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Cardo:ital,wght@0,400;0,700;1,400',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 20
|
||||
},
|
||||
'Gentium Plus': {
|
||||
family: 'Gentium Plus',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Gentium+Plus:ital,wght@0,400;0,700;1,400;1,700',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 18
|
||||
},
|
||||
'Neuton': {
|
||||
family: 'Neuton',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Neuton:ital,wght@0,200;0,300;0,400;0,700;0,800;1,400',
|
||||
weights: [200, 300, 400, 700, 800],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 15
|
||||
},
|
||||
'Bitter': {
|
||||
family: 'Bitter',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Bitter:ital,wght@0,100..900;1,100..900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Georgia', 'Times New Roman', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif',
|
||||
popularity: 12
|
||||
},
|
||||
|
||||
// Monospace fonts (10 fonts)
|
||||
'JetBrains Mono': {
|
||||
family: 'JetBrains Mono',
|
||||
url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800],
|
||||
fallbacks: ['Consolas', 'Monaco', 'Courier New', 'monospace'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'monospace',
|
||||
popularity: 82
|
||||
},
|
||||
'Fira Code': {
|
||||
family: 'Fira Code',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700',
|
||||
weights: [300, 400, 500, 600, 700],
|
||||
fallbacks: ['Consolas', 'Monaco', 'Courier New', 'monospace'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'monospace',
|
||||
popularity: 78
|
||||
},
|
||||
'Source Code Pro': {
|
||||
family: 'Source Code Pro',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900',
|
||||
weights: [200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Consolas', 'Monaco', 'Courier New', 'monospace'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'monospace',
|
||||
popularity: 75
|
||||
},
|
||||
'Roboto Mono': {
|
||||
family: 'Roboto Mono',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700],
|
||||
fallbacks: ['Consolas', 'Monaco', 'Courier New', 'monospace'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'monospace',
|
||||
popularity: 70
|
||||
},
|
||||
'IBM Plex Mono': {
|
||||
family: 'IBM Plex Mono',
|
||||
url: 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700],
|
||||
fallbacks: ['Consolas', 'Monaco', 'Courier New', 'monospace'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'monospace',
|
||||
popularity: 65
|
||||
},
|
||||
'Space Mono': {
|
||||
family: 'Space Mono',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['Consolas', 'Monaco', 'Courier New', 'monospace'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'monospace',
|
||||
popularity: 60
|
||||
},
|
||||
'Inconsolata': {
|
||||
family: 'Inconsolata',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Inconsolata:wght@200..900',
|
||||
weights: [200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Consolas', 'Monaco', 'Courier New', 'monospace'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'monospace',
|
||||
popularity: 55
|
||||
},
|
||||
'PT Mono': {
|
||||
family: 'PT Mono',
|
||||
url: 'https://fonts.googleapis.com/css2?family=PT+Mono',
|
||||
weights: [400],
|
||||
fallbacks: ['Consolas', 'Monaco', 'Courier New', 'monospace'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'monospace',
|
||||
popularity: 50
|
||||
},
|
||||
'Fira Mono': {
|
||||
family: 'Fira Mono',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700',
|
||||
weights: [400, 500, 700],
|
||||
fallbacks: ['Consolas', 'Monaco', 'Courier New', 'monospace'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'monospace',
|
||||
popularity: 45
|
||||
},
|
||||
'Ubuntu Mono': {
|
||||
family: 'Ubuntu Mono',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400;1,700',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['Consolas', 'Monaco', 'Courier New', 'monospace'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'monospace',
|
||||
popularity: 40
|
||||
},
|
||||
|
||||
// Display fonts (10 fonts)
|
||||
'Oswald': {
|
||||
family: 'Oswald',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Oswald:wght@200..700',
|
||||
weights: [200, 300, 400, 500, 600, 700],
|
||||
fallbacks: ['Impact', 'Arial Black', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'display',
|
||||
popularity: 80
|
||||
},
|
||||
'Bebas Neue': {
|
||||
family: 'Bebas Neue',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Bebas+Neue',
|
||||
weights: [400],
|
||||
fallbacks: ['Impact', 'Arial Black', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'display',
|
||||
popularity: 72
|
||||
},
|
||||
'Anton': {
|
||||
family: 'Anton',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Anton',
|
||||
weights: [400],
|
||||
fallbacks: ['Impact', 'Arial Black', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'display',
|
||||
popularity: 68
|
||||
},
|
||||
'Righteous': {
|
||||
family: 'Righteous',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Righteous',
|
||||
weights: [400],
|
||||
fallbacks: ['Impact', 'Arial Black', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'display',
|
||||
popularity: 65
|
||||
},
|
||||
'Fredoka One': {
|
||||
family: 'Fredoka One',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Fredoka+One',
|
||||
weights: [400],
|
||||
fallbacks: ['Impact', 'Arial Black', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'display',
|
||||
popularity: 60
|
||||
},
|
||||
'Archivo Black': {
|
||||
family: 'Archivo Black',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Archivo+Black',
|
||||
weights: [400],
|
||||
fallbacks: ['Impact', 'Arial Black', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'display',
|
||||
popularity: 55
|
||||
},
|
||||
'Cabin': {
|
||||
family: 'Cabin',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Cabin:ital,wght@0,400..700;1,400..700',
|
||||
weights: [400, 500, 600, 700],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'display',
|
||||
popularity: 52
|
||||
},
|
||||
'Prompt': {
|
||||
family: 'Prompt',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Prompt:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900',
|
||||
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||
fallbacks: ['Arial', 'Helvetica', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'display',
|
||||
popularity: 48
|
||||
},
|
||||
'Fjalla One': {
|
||||
family: 'Fjalla One',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Fjalla+One',
|
||||
weights: [400],
|
||||
fallbacks: ['Impact', 'Arial Black', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'display',
|
||||
popularity: 45
|
||||
},
|
||||
'Patua One': {
|
||||
family: 'Patua One',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Patua+One',
|
||||
weights: [400],
|
||||
fallbacks: ['Impact', 'Arial Black', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'display',
|
||||
popularity: 42
|
||||
},
|
||||
|
||||
// Handwriting fonts (10 fonts)
|
||||
'Dancing Script': {
|
||||
family: 'Dancing Script',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400..700',
|
||||
weights: [400, 500, 600, 700],
|
||||
fallbacks: ['cursive'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'handwriting',
|
||||
popularity: 68
|
||||
},
|
||||
'Great Vibes': {
|
||||
family: 'Great Vibes',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Great+Vibes',
|
||||
weights: [400],
|
||||
fallbacks: ['cursive'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'handwriting',
|
||||
popularity: 65
|
||||
},
|
||||
'Pacifico': {
|
||||
family: 'Pacifico',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Pacifico',
|
||||
weights: [400],
|
||||
fallbacks: ['cursive'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'handwriting',
|
||||
popularity: 62
|
||||
},
|
||||
'Kaushan Script': {
|
||||
family: 'Kaushan Script',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Kaushan+Script',
|
||||
weights: [400],
|
||||
fallbacks: ['cursive'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'handwriting',
|
||||
popularity: 58
|
||||
},
|
||||
'Satisfy': {
|
||||
family: 'Satisfy',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Satisfy',
|
||||
weights: [400],
|
||||
fallbacks: ['cursive'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'handwriting',
|
||||
popularity: 55
|
||||
},
|
||||
'Caveat': {
|
||||
family: 'Caveat',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Caveat:wght@400..700',
|
||||
weights: [400, 500, 600, 700],
|
||||
fallbacks: ['cursive'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'handwriting',
|
||||
popularity: 52
|
||||
},
|
||||
'Amatic SC': {
|
||||
family: 'Amatic SC',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Amatic+SC:wght@400;700',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['cursive'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'handwriting',
|
||||
popularity: 48
|
||||
},
|
||||
'Indie Flower': {
|
||||
family: 'Indie Flower',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Indie+Flower',
|
||||
weights: [400],
|
||||
fallbacks: ['cursive'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'handwriting',
|
||||
popularity: 45
|
||||
},
|
||||
'Shadows Into Light': {
|
||||
family: 'Shadows Into Light',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Shadows+Into+Light',
|
||||
weights: [400],
|
||||
fallbacks: ['cursive'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'handwriting',
|
||||
popularity: 42
|
||||
},
|
||||
'Permanent Marker': {
|
||||
family: 'Permanent Marker',
|
||||
url: 'https://fonts.googleapis.com/css2?family=Permanent+Marker',
|
||||
weights: [400],
|
||||
fallbacks: ['cursive'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'handwriting',
|
||||
popularity: 38
|
||||
}
|
||||
};
|
||||
|
||||
// Popular font combinations
|
||||
export const FONT_COMBINATIONS = {
|
||||
'Modern Clean': {
|
||||
sans: 'Inter',
|
||||
serif: 'Playfair Display',
|
||||
mono: 'JetBrains Mono',
|
||||
display: 'Inter'
|
||||
},
|
||||
'Classic Editorial': {
|
||||
sans: 'Source Sans Pro',
|
||||
serif: 'Lora',
|
||||
mono: 'Source Code Pro',
|
||||
display: 'Oswald'
|
||||
},
|
||||
'Friendly Modern': {
|
||||
sans: 'Poppins',
|
||||
serif: 'Merriweather',
|
||||
mono: 'Fira Code',
|
||||
display: 'Raleway'
|
||||
},
|
||||
'Professional': {
|
||||
sans: 'Roboto',
|
||||
serif: 'EB Garamond',
|
||||
mono: 'Roboto Mono',
|
||||
display: 'Montserrat'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,286 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
export interface FontLoadingState {
|
||||
fontName: string;
|
||||
status: 'idle' | 'loading' | 'loaded' | 'error' | 'timeout';
|
||||
progress: number; // 0-100
|
||||
error?: string;
|
||||
loadTime?: number;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface GlobalLoadingState {
|
||||
isLoading: boolean;
|
||||
totalFonts: number;
|
||||
loadedFonts: number;
|
||||
failedFonts: number;
|
||||
progress: number; // 0-100
|
||||
currentlyLoading: string[];
|
||||
errors: { [fontName: string]: string };
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FontLoadingStateService {
|
||||
private fontStates = new Map<string, BehaviorSubject<FontLoadingState>>();
|
||||
private globalState$ = new BehaviorSubject<GlobalLoadingState>({
|
||||
isLoading: false,
|
||||
totalFonts: 0,
|
||||
loadedFonts: 0,
|
||||
failedFonts: 0,
|
||||
progress: 0,
|
||||
currentlyLoading: [],
|
||||
errors: {}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get loading state for a specific font
|
||||
*/
|
||||
getFontState(fontName: string): Observable<FontLoadingState> {
|
||||
if (!this.fontStates.has(fontName)) {
|
||||
this.fontStates.set(fontName, new BehaviorSubject<FontLoadingState>({
|
||||
fontName,
|
||||
status: 'idle',
|
||||
progress: 0
|
||||
}));
|
||||
}
|
||||
|
||||
return this.fontStates.get(fontName)!.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global loading state
|
||||
*/
|
||||
getGlobalState(): Observable<GlobalLoadingState> {
|
||||
return this.globalState$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all font states
|
||||
*/
|
||||
getAllFontStates(): Observable<FontLoadingState[]> {
|
||||
if (this.fontStates.size === 0) {
|
||||
return new BehaviorSubject<FontLoadingState[]>([]).asObservable();
|
||||
}
|
||||
|
||||
const stateObservables = Array.from(this.fontStates.values());
|
||||
return combineLatest(stateObservables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update font loading state
|
||||
*/
|
||||
updateFontState(fontName: string, update: Partial<FontLoadingState>): void {
|
||||
const currentSubject = this.fontStates.get(fontName);
|
||||
if (!currentSubject) {
|
||||
this.fontStates.set(fontName, new BehaviorSubject<FontLoadingState>({
|
||||
fontName,
|
||||
status: 'idle',
|
||||
progress: 0,
|
||||
...update
|
||||
}));
|
||||
} else {
|
||||
const current = currentSubject.value;
|
||||
currentSubject.next({ ...current, ...update });
|
||||
}
|
||||
|
||||
this.updateGlobalState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start font loading
|
||||
*/
|
||||
startLoading(fontName: string, url?: string): void {
|
||||
this.updateFontState(fontName, {
|
||||
status: 'loading',
|
||||
progress: 0,
|
||||
url,
|
||||
error: undefined,
|
||||
loadTime: undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update loading progress
|
||||
*/
|
||||
updateProgress(fontName: string, progress: number): void {
|
||||
this.updateFontState(fontName, {
|
||||
progress: Math.max(0, Math.min(100, progress))
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark font as loaded
|
||||
*/
|
||||
markLoaded(fontName: string, loadTime: number): void {
|
||||
this.updateFontState(fontName, {
|
||||
status: 'loaded',
|
||||
progress: 100,
|
||||
loadTime
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark font as failed
|
||||
*/
|
||||
markFailed(fontName: string, error: string): void {
|
||||
this.updateFontState(fontName, {
|
||||
status: 'error',
|
||||
progress: 0,
|
||||
error
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark font as timed out
|
||||
*/
|
||||
markTimeout(fontName: string): void {
|
||||
this.updateFontState(fontName, {
|
||||
status: 'timeout',
|
||||
progress: 0,
|
||||
error: 'Font loading timed out'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset font state
|
||||
*/
|
||||
resetFontState(fontName: string): void {
|
||||
this.updateFontState(fontName, {
|
||||
status: 'idle',
|
||||
progress: 0,
|
||||
error: undefined,
|
||||
loadTime: undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all font states
|
||||
*/
|
||||
resetAllStates(): void {
|
||||
this.fontStates.forEach((subject, fontName) => {
|
||||
subject.next({
|
||||
fontName,
|
||||
status: 'idle',
|
||||
progress: 0
|
||||
});
|
||||
});
|
||||
|
||||
this.updateGlobalState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove font state
|
||||
*/
|
||||
removeFontState(fontName: string): void {
|
||||
const subject = this.fontStates.get(fontName);
|
||||
if (subject) {
|
||||
subject.complete();
|
||||
this.fontStates.delete(fontName);
|
||||
}
|
||||
|
||||
this.updateGlobalState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get loading statistics
|
||||
*/
|
||||
getLoadingStats(): Observable<{
|
||||
totalRequests: number;
|
||||
successRate: number;
|
||||
averageLoadTime: number;
|
||||
fastestLoad: number;
|
||||
slowestLoad: number;
|
||||
}> {
|
||||
return this.getAllFontStates().pipe(
|
||||
map(states => {
|
||||
const completedStates = states.filter(s => s.status === 'loaded' || s.status === 'error');
|
||||
const loadedStates = states.filter(s => s.status === 'loaded');
|
||||
const loadTimes = loadedStates
|
||||
.map(s => s.loadTime)
|
||||
.filter(time => time !== undefined) as number[];
|
||||
|
||||
return {
|
||||
totalRequests: states.length,
|
||||
successRate: completedStates.length > 0 ? (loadedStates.length / completedStates.length) * 100 : 0,
|
||||
averageLoadTime: loadTimes.length > 0 ? loadTimes.reduce((a, b) => a + b, 0) / loadTimes.length : 0,
|
||||
fastestLoad: loadTimes.length > 0 ? Math.min(...loadTimes) : 0,
|
||||
slowestLoad: loadTimes.length > 0 ? Math.max(...loadTimes) : 0
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any fonts are currently loading
|
||||
*/
|
||||
isAnyFontLoading(): Observable<boolean> {
|
||||
return this.getGlobalState().pipe(
|
||||
map(state => state.isLoading)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fonts by status
|
||||
*/
|
||||
getFontsByStatus(status: FontLoadingState['status']): Observable<FontLoadingState[]> {
|
||||
return this.getAllFontStates().pipe(
|
||||
map(states => states.filter(state => state.status === status))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all fonts to finish loading (success or failure)
|
||||
*/
|
||||
waitForAllFonts(): Observable<FontLoadingState[]> {
|
||||
return this.getAllFontStates().pipe(
|
||||
map(states => {
|
||||
const allFinished = states.every(state =>
|
||||
state.status === 'loaded' ||
|
||||
state.status === 'error' ||
|
||||
state.status === 'timeout' ||
|
||||
state.status === 'idle'
|
||||
);
|
||||
|
||||
if (allFinished) {
|
||||
return states;
|
||||
} else {
|
||||
throw new Error('Fonts still loading'); // This will cause the observable to not emit yet
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private updateGlobalState(): void {
|
||||
const states = Array.from(this.fontStates.values()).map(subject => subject.value);
|
||||
|
||||
const totalFonts = states.length;
|
||||
const loadingFonts = states.filter(s => s.status === 'loading');
|
||||
const loadedFonts = states.filter(s => s.status === 'loaded');
|
||||
const failedFonts = states.filter(s => s.status === 'error' || s.status === 'timeout');
|
||||
const currentlyLoading = loadingFonts.map(s => s.fontName);
|
||||
|
||||
const errors: { [fontName: string]: string } = {};
|
||||
states.forEach(state => {
|
||||
if (state.error) {
|
||||
errors[state.fontName] = state.error;
|
||||
}
|
||||
});
|
||||
|
||||
const completedFonts = loadedFonts.length + failedFonts.length;
|
||||
const progress = totalFonts > 0 ? (completedFonts / totalFonts) * 100 : 0;
|
||||
|
||||
this.globalState$.next({
|
||||
isLoading: loadingFonts.length > 0,
|
||||
totalFonts,
|
||||
loadedFonts: loadedFonts.length,
|
||||
failedFonts: failedFonts.length,
|
||||
progress,
|
||||
currentlyLoading,
|
||||
errors
|
||||
});
|
||||
}
|
||||
}
|
||||
210
projects/ui-font-manager/src/lib/services/font-theme.service.ts
Normal file
210
projects/ui-font-manager/src/lib/services/font-theme.service.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { FontTheme, FontDefinition } from '../types/font-types';
|
||||
import { UiFontManagerService } from '../ui-font-manager.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FontThemeService {
|
||||
private themes = new Map<string, FontTheme>();
|
||||
private activeThemeSubject = new BehaviorSubject<FontTheme | null>(null);
|
||||
|
||||
constructor(private fontManager: UiFontManagerService) {
|
||||
this.initializeDefaultThemes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available themes
|
||||
*/
|
||||
getThemes(): FontTheme[] {
|
||||
return Array.from(this.themes.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme by name
|
||||
*/
|
||||
getTheme(name: string): FontTheme | undefined {
|
||||
return this.themes.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a theme
|
||||
*/
|
||||
addTheme(theme: FontTheme): void {
|
||||
this.themes.set(theme.name, theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a theme
|
||||
*/
|
||||
removeTheme(name: string): boolean {
|
||||
return this.themes.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a theme by name
|
||||
*/
|
||||
applyTheme(themeName: string): Observable<boolean> {
|
||||
const theme = this.themes.get(themeName);
|
||||
if (!theme) {
|
||||
console.warn(`Theme "${themeName}" not found`);
|
||||
return new BehaviorSubject(false).asObservable();
|
||||
}
|
||||
|
||||
return new Observable(observer => {
|
||||
this.fontManager.applyTheme(theme).subscribe({
|
||||
next: (success) => {
|
||||
if (success) {
|
||||
this.activeThemeSubject.next(theme);
|
||||
}
|
||||
observer.next(success);
|
||||
observer.complete();
|
||||
},
|
||||
error: (error) => observer.error(error)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently active theme
|
||||
*/
|
||||
getActiveTheme(): Observable<FontTheme | null> {
|
||||
return this.activeThemeSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create theme from font combination
|
||||
*/
|
||||
createThemeFromCombination(name: string, combinationName: string, description?: string): FontTheme | null {
|
||||
const theme = this.fontManager.createThemeFromCombination(name, combinationName);
|
||||
if (theme && description) {
|
||||
theme.description = description;
|
||||
}
|
||||
if (theme) {
|
||||
this.addTheme(theme);
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom theme
|
||||
*/
|
||||
createCustomTheme(
|
||||
name: string,
|
||||
fonts: {
|
||||
sans: string;
|
||||
serif: string;
|
||||
mono: string;
|
||||
display: string;
|
||||
},
|
||||
description?: string
|
||||
): FontTheme | null {
|
||||
const allFonts = this.fontManager.getAllFonts();
|
||||
|
||||
// Validate all fonts exist
|
||||
const fontDefinitions: { [key: string]: FontDefinition } = {};
|
||||
for (const [type, fontName] of Object.entries(fonts)) {
|
||||
const font = allFonts[fontName];
|
||||
if (!font) {
|
||||
console.warn(`Font "${fontName}" not found for type "${type}"`);
|
||||
return null;
|
||||
}
|
||||
fontDefinitions[type] = font;
|
||||
}
|
||||
|
||||
const theme: FontTheme = {
|
||||
name,
|
||||
description,
|
||||
fonts: {
|
||||
sans: fontDefinitions['sans'],
|
||||
serif: fontDefinitions['serif'],
|
||||
mono: fontDefinitions['mono'],
|
||||
display: fontDefinitions['display']
|
||||
}
|
||||
};
|
||||
|
||||
this.addTheme(theme);
|
||||
return theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export theme configuration
|
||||
*/
|
||||
exportTheme(themeName: string): string | null {
|
||||
const theme = this.themes.get(themeName);
|
||||
if (!theme) return null;
|
||||
|
||||
return JSON.stringify(theme, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import theme from configuration
|
||||
*/
|
||||
importTheme(themeJson: string): FontTheme | null {
|
||||
try {
|
||||
const theme: FontTheme = JSON.parse(themeJson);
|
||||
|
||||
// Validate theme structure
|
||||
if (!theme.name || !theme.fonts) {
|
||||
throw new Error('Invalid theme structure');
|
||||
}
|
||||
|
||||
this.addTheme(theme);
|
||||
return theme;
|
||||
} catch (error) {
|
||||
console.error('Failed to import theme:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private initializeDefaultThemes(): void {
|
||||
const combinations = this.fontManager.getFontCombinations();
|
||||
|
||||
// Create default themes from combinations
|
||||
Object.entries(combinations).forEach(([name, combo]) => {
|
||||
const theme = this.fontManager.createThemeFromCombination(name, name);
|
||||
if (theme) {
|
||||
this.addTheme(theme);
|
||||
}
|
||||
});
|
||||
|
||||
// Add system fonts theme
|
||||
const systemTheme: FontTheme = {
|
||||
name: 'System',
|
||||
description: 'Use system default fonts for optimal performance',
|
||||
fonts: {
|
||||
sans: {
|
||||
family: 'system-ui',
|
||||
weights: [400, 500, 600, 700],
|
||||
fallbacks: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'sans-serif'
|
||||
},
|
||||
serif: {
|
||||
family: 'ui-serif',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['Georgia', 'Cambria', 'Times New Roman', 'Times', 'serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'serif'
|
||||
},
|
||||
mono: {
|
||||
family: 'ui-monospace',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['SFMono-Regular', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', 'monospace'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'monospace'
|
||||
},
|
||||
display: {
|
||||
family: 'system-ui',
|
||||
weights: [400, 700],
|
||||
fallbacks: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||
loadingStrategy: 'swap',
|
||||
category: 'display'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.addTheme(systemTheme);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
import { Injectable, Inject } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
export interface FontTransitionConfig {
|
||||
duration: number; // in milliseconds
|
||||
easing: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | string;
|
||||
stagger: number; // delay between elements in milliseconds
|
||||
fadeEffect: boolean;
|
||||
scaleEffect: boolean;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FontTransitionService {
|
||||
private isTransitioning$ = new BehaviorSubject<boolean>(false);
|
||||
private transitionConfig: FontTransitionConfig = {
|
||||
duration: 300,
|
||||
easing: 'ease-in-out',
|
||||
stagger: 50,
|
||||
fadeEffect: true,
|
||||
scaleEffect: false
|
||||
};
|
||||
|
||||
constructor(@Inject(DOCUMENT) private document: Document) {}
|
||||
|
||||
/**
|
||||
* Configure transition settings
|
||||
*/
|
||||
configure(config: Partial<FontTransitionConfig>): void {
|
||||
this.transitionConfig = { ...this.transitionConfig, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current transition state
|
||||
*/
|
||||
isTransitioning(): Observable<boolean> {
|
||||
return this.isTransitioning$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply smooth font transition with animations
|
||||
*/
|
||||
applyFontTransition(
|
||||
newFontVariables: { [key: string]: string },
|
||||
customConfig?: Partial<FontTransitionConfig>
|
||||
): Observable<void> {
|
||||
return new Observable(observer => {
|
||||
const config = customConfig ? { ...this.transitionConfig, ...customConfig } : this.transitionConfig;
|
||||
|
||||
this.isTransitioning$.next(true);
|
||||
|
||||
// Get all text elements
|
||||
const textElements = this.getTextElements();
|
||||
|
||||
// Apply transition styles
|
||||
this.prepareTransition(textElements, config);
|
||||
|
||||
// Start the transition
|
||||
requestAnimationFrame(() => {
|
||||
this.executeTransition(textElements, newFontVariables, config).then(() => {
|
||||
this.isTransitioning$.next(false);
|
||||
observer.next();
|
||||
observer.complete();
|
||||
}).catch(error => {
|
||||
this.isTransitioning$.next(false);
|
||||
observer.error(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply font changes with smooth fade transition
|
||||
*/
|
||||
applyWithFade(newFontVariables: { [key: string]: string }): Observable<void> {
|
||||
return this.applyFontTransition(newFontVariables, {
|
||||
fadeEffect: true,
|
||||
scaleEffect: false,
|
||||
duration: 400,
|
||||
easing: 'ease-in-out'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply font changes with scale effect
|
||||
*/
|
||||
applyWithScale(newFontVariables: { [key: string]: string }): Observable<void> {
|
||||
return this.applyFontTransition(newFontVariables, {
|
||||
fadeEffect: false,
|
||||
scaleEffect: true,
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply instant font changes without animation
|
||||
*/
|
||||
applyInstant(newFontVariables: { [key: string]: string }): void {
|
||||
const root = this.document.documentElement;
|
||||
Object.entries(newFontVariables).forEach(([property, value]) => {
|
||||
root.style.setProperty(property, value);
|
||||
});
|
||||
}
|
||||
|
||||
private getTextElements(): Element[] {
|
||||
// Get all elements that likely contain text
|
||||
const selectors = [
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'p', 'span', 'div', 'a', 'button',
|
||||
'label', 'input', 'textarea', 'select',
|
||||
'.text', '[class*="text"]', '[class*="font"]'
|
||||
];
|
||||
|
||||
const elements: Element[] = [];
|
||||
selectors.forEach(selector => {
|
||||
const found = Array.from(this.document.querySelectorAll(selector));
|
||||
elements.push(...found);
|
||||
});
|
||||
|
||||
// Remove duplicates and filter out elements without text content
|
||||
return Array.from(new Set(elements)).filter(el => {
|
||||
const text = el.textContent?.trim();
|
||||
return text && text.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
private prepareTransition(elements: Element[], config: FontTransitionConfig): void {
|
||||
elements.forEach((element, index) => {
|
||||
const htmlElement = element as HTMLElement;
|
||||
|
||||
// Store original styles
|
||||
const originalTransition = htmlElement.style.transition;
|
||||
htmlElement.setAttribute('data-original-transition', originalTransition);
|
||||
|
||||
// Apply transition styles
|
||||
const transitions: string[] = [];
|
||||
|
||||
if (config.fadeEffect) {
|
||||
transitions.push(`opacity ${config.duration}ms ${config.easing}`);
|
||||
}
|
||||
|
||||
if (config.scaleEffect) {
|
||||
transitions.push(`transform ${config.duration}ms ${config.easing}`);
|
||||
}
|
||||
|
||||
// Always transition font properties
|
||||
transitions.push(
|
||||
`font-family ${config.duration}ms ${config.easing}`,
|
||||
`font-weight ${config.duration}ms ${config.easing}`,
|
||||
`font-size ${config.duration}ms ${config.easing}`
|
||||
);
|
||||
|
||||
htmlElement.style.transition = transitions.join(', ');
|
||||
|
||||
// Add stagger delay
|
||||
if (config.stagger > 0) {
|
||||
htmlElement.style.transitionDelay = `${index * config.stagger}ms`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async executeTransition(
|
||||
elements: Element[],
|
||||
newFontVariables: { [key: string]: string },
|
||||
config: FontTransitionConfig
|
||||
): Promise<void> {
|
||||
// Phase 1: Prepare for transition (fade out / scale down)
|
||||
if (config.fadeEffect || config.scaleEffect) {
|
||||
elements.forEach(element => {
|
||||
const htmlElement = element as HTMLElement;
|
||||
|
||||
if (config.fadeEffect) {
|
||||
htmlElement.style.opacity = '0.3';
|
||||
}
|
||||
|
||||
if (config.scaleEffect) {
|
||||
htmlElement.style.transform = 'scale(0.98)';
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for fade/scale animation
|
||||
await this.delay(config.duration / 2);
|
||||
}
|
||||
|
||||
// Phase 2: Apply new font variables
|
||||
const root = this.document.documentElement;
|
||||
Object.entries(newFontVariables).forEach(([property, value]) => {
|
||||
root.style.setProperty(property, value);
|
||||
});
|
||||
|
||||
// Small delay to ensure font variables are applied
|
||||
await this.delay(10);
|
||||
|
||||
// Phase 3: Restore visibility (fade in / scale up)
|
||||
if (config.fadeEffect || config.scaleEffect) {
|
||||
elements.forEach(element => {
|
||||
const htmlElement = element as HTMLElement;
|
||||
|
||||
if (config.fadeEffect) {
|
||||
htmlElement.style.opacity = '1';
|
||||
}
|
||||
|
||||
if (config.scaleEffect) {
|
||||
htmlElement.style.transform = 'scale(1)';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Phase 4: Wait for transition to complete and cleanup
|
||||
const maxDelay = elements.length * config.stagger;
|
||||
const totalDuration = config.duration + maxDelay + 50; // Add buffer
|
||||
|
||||
await this.delay(totalDuration);
|
||||
|
||||
// Cleanup: Restore original transition styles
|
||||
elements.forEach(element => {
|
||||
const htmlElement = element as HTMLElement;
|
||||
const originalTransition = htmlElement.getAttribute('data-original-transition') || '';
|
||||
|
||||
htmlElement.style.transition = originalTransition;
|
||||
htmlElement.style.transitionDelay = '';
|
||||
htmlElement.style.opacity = '';
|
||||
htmlElement.style.transform = '';
|
||||
htmlElement.removeAttribute('data-original-transition');
|
||||
});
|
||||
}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom transition keyframe animation
|
||||
*/
|
||||
createCustomTransition(keyframes: Keyframe[], options: KeyframeAnimationOptions): void {
|
||||
const textElements = this.getTextElements();
|
||||
|
||||
textElements.forEach((element, index) => {
|
||||
const animation = element.animate(keyframes, {
|
||||
...options,
|
||||
delay: (options.delay || 0) + (index * 25) // Stagger effect
|
||||
});
|
||||
|
||||
animation.addEventListener('finish', () => {
|
||||
animation.cancel();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Typewriter effect for font changes
|
||||
*/
|
||||
applyTypewriterTransition(newFontVariables: { [key: string]: string }): Observable<void> {
|
||||
return new Observable(observer => {
|
||||
const textElements = this.getTextElements();
|
||||
let completedElements = 0;
|
||||
|
||||
this.isTransitioning$.next(true);
|
||||
|
||||
// Apply new fonts immediately but keep text invisible
|
||||
const root = this.document.documentElement;
|
||||
Object.entries(newFontVariables).forEach(([property, value]) => {
|
||||
root.style.setProperty(property, value);
|
||||
});
|
||||
|
||||
textElements.forEach((element, index) => {
|
||||
const htmlElement = element as HTMLElement;
|
||||
const text = htmlElement.textContent || '';
|
||||
const originalText = text;
|
||||
|
||||
// Clear text and start typewriter effect
|
||||
htmlElement.textContent = '';
|
||||
|
||||
let charIndex = 0;
|
||||
const typeInterval = setInterval(() => {
|
||||
if (charIndex < originalText.length) {
|
||||
htmlElement.textContent = originalText.substring(0, charIndex + 1);
|
||||
charIndex++;
|
||||
} else {
|
||||
clearInterval(typeInterval);
|
||||
completedElements++;
|
||||
|
||||
if (completedElements === textElements.length) {
|
||||
this.isTransitioning$.next(false);
|
||||
observer.next();
|
||||
observer.complete();
|
||||
}
|
||||
}
|
||||
}, 20 + (index * 5)); // Stagger the typing speed
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
38
projects/ui-font-manager/src/lib/types/font-types.ts
Normal file
38
projects/ui-font-manager/src/lib/types/font-types.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export interface FontDefinition {
|
||||
family: string;
|
||||
url?: string;
|
||||
weights: number[];
|
||||
fallbacks: string[];
|
||||
loadingStrategy: 'eager' | 'lazy' | 'swap';
|
||||
category: 'sans-serif' | 'serif' | 'monospace' | 'display' | 'handwriting';
|
||||
popularity?: number;
|
||||
}
|
||||
|
||||
export interface FontTheme {
|
||||
name: string;
|
||||
description?: string;
|
||||
fonts: {
|
||||
sans: FontDefinition;
|
||||
serif: FontDefinition;
|
||||
mono: FontDefinition;
|
||||
display: FontDefinition;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FontPreset {
|
||||
[key: string]: FontDefinition;
|
||||
}
|
||||
|
||||
export interface FontLoadingOptions {
|
||||
display?: 'auto' | 'block' | 'swap' | 'fallback' | 'optional';
|
||||
weights?: number[];
|
||||
subsets?: string[];
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface FontManagerConfig {
|
||||
defaultTheme?: string;
|
||||
preloadFonts?: boolean;
|
||||
fallbackTimeout?: number;
|
||||
cacheEnabled?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UiFontManagerComponent } from './ui-font-manager.component';
|
||||
|
||||
describe('UiFontManagerComponent', () => {
|
||||
let component: UiFontManagerComponent;
|
||||
let fixture: ComponentFixture<UiFontManagerComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UiFontManagerComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UiFontManagerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
314
projects/ui-font-manager/src/lib/ui-font-manager.component.ts
Normal file
314
projects/ui-font-manager/src/lib/ui-font-manager.component.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
import { Component, Input, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Observable, Subject, takeUntil } from 'rxjs';
|
||||
import { UiFontManagerService } from './ui-font-manager.service';
|
||||
import { FontThemeService } from './services/font-theme.service';
|
||||
import { FontLoadingStateService, GlobalLoadingState } from './services/font-loading-state.service';
|
||||
import { FontTransitionService } from './services/font-transition.service';
|
||||
import { FontTheme, FontDefinition } from './types/font-types';
|
||||
|
||||
@Component({
|
||||
selector: 'lib-ui-font-manager',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<div class="font-manager" [class.loading]="(loadingState$ | async)?.isLoading">
|
||||
|
||||
<!-- Font Theme Selector -->
|
||||
<div class="theme-selector" *ngIf="showThemeSelector">
|
||||
<label for="theme-select">Font Theme:</label>
|
||||
<select
|
||||
id="theme-select"
|
||||
[value]="selectedTheme"
|
||||
(change)="onThemeChange($event)"
|
||||
[disabled]="(loadingState$ | async)?.isLoading">
|
||||
<option value="">Select a theme...</option>
|
||||
<option *ngFor="let theme of availableThemes" [value]="theme.name">
|
||||
{{theme.name}} - {{theme.description}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Loading Progress -->
|
||||
<div class="loading-progress" *ngIf="(loadingState$ | async)?.isLoading">
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
[style.width.%]="(loadingState$ | async)?.progress || 0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-info">
|
||||
Loading fonts: {{(loadingState$ | async)?.loadedFonts}}/{{(loadingState$ | async)?.totalFonts}}
|
||||
<span *ngIf="(loadingState$ | async)?.currentlyLoading?.length">
|
||||
({{(loadingState$ | async)?.currentlyLoading?.join(', ')}})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Display -->
|
||||
<div class="error-messages" *ngIf="hasErrors">
|
||||
<div *ngFor="let error of errorMessages" class="error-message">
|
||||
<strong>{{error.fontName}}:</strong> {{error.message}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Font Preview -->
|
||||
<div class="font-preview" *ngIf="showPreview">
|
||||
<div class="preview-text sans" style="font-family: var(--font-family-sans, inherit)">
|
||||
Sans-serif: The quick brown fox jumps over the lazy dog
|
||||
</div>
|
||||
<div class="preview-text serif" style="font-family: var(--font-family-serif, inherit)">
|
||||
Serif: The quick brown fox jumps over the lazy dog
|
||||
</div>
|
||||
<div class="preview-text mono" style="font-family: var(--font-family-mono, inherit)">
|
||||
Mono: The quick brown fox jumps over the lazy dog
|
||||
</div>
|
||||
<div class="preview-text display" style="font-family: var(--font-family-display, inherit)">
|
||||
Display: The quick brown fox jumps over the lazy dog
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
.font-manager {
|
||||
padding: 1rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.font-manager.loading {
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.theme-selector {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.theme-selector label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.theme-selector select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.loading-progress {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4CAF50, #2196F3);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-info {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-messages {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 0.5rem;
|
||||
background: #ffebee;
|
||||
border: 1px solid #f44336;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.font-preview {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #2196F3;
|
||||
}
|
||||
|
||||
.preview-text.sans { border-left-color: #4CAF50; }
|
||||
.preview-text.serif { border-left-color: #FF9800; }
|
||||
.preview-text.mono {
|
||||
border-left-color: #9C27B0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.preview-text.display {
|
||||
border-left-color: #f44336;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
`
|
||||
})
|
||||
export class UiFontManagerComponent implements OnInit, OnDestroy {
|
||||
@Input() showThemeSelector: boolean = true;
|
||||
@Input() showPreview: boolean = true;
|
||||
@Input() showLoadingProgress: boolean = true;
|
||||
@Input() previewText: string = 'The quick brown fox jumps over the lazy dog';
|
||||
|
||||
@Output() themeChanged = new EventEmitter<string>();
|
||||
@Output() fontLoaded = new EventEmitter<string>();
|
||||
@Output() fontError = new EventEmitter<{fontName: string, error: string}>();
|
||||
|
||||
availableThemes: FontTheme[] = [];
|
||||
selectedTheme: string = '';
|
||||
loadingState$: Observable<GlobalLoadingState>;
|
||||
hasErrors: boolean = false;
|
||||
errorMessages: {fontName: string, message: string}[] = [];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private fontManager: UiFontManagerService,
|
||||
private themeService: FontThemeService,
|
||||
private loadingStateService: FontLoadingStateService,
|
||||
private transitionService: FontTransitionService
|
||||
) {
|
||||
this.loadingState$ = this.loadingStateService.getGlobalState();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Load available themes
|
||||
this.availableThemes = this.themeService.getThemes();
|
||||
|
||||
// Monitor loading state for errors
|
||||
this.loadingState$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(state => {
|
||||
this.hasErrors = Object.keys(state.errors).length > 0;
|
||||
this.errorMessages = Object.entries(state.errors).map(([fontName, message]) => ({
|
||||
fontName,
|
||||
message
|
||||
}));
|
||||
});
|
||||
|
||||
// Listen for active theme changes
|
||||
this.themeService.getActiveTheme()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(theme => {
|
||||
if (theme) {
|
||||
this.selectedTheme = theme.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
onThemeChange(event: Event): void {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
const themeName = target.value;
|
||||
|
||||
if (!themeName) return;
|
||||
|
||||
this.selectedTheme = themeName;
|
||||
|
||||
// Apply theme with smooth transition
|
||||
this.themeService.applyTheme(themeName)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (success) => {
|
||||
if (success) {
|
||||
this.themeChanged.emit(themeName);
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to apply theme:', error);
|
||||
this.fontError.emit({fontName: themeName, error: error.message});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically load a font
|
||||
*/
|
||||
loadFont(fontName: string): void {
|
||||
this.fontManager.loadFont(fontName)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (success) => {
|
||||
if (success) {
|
||||
this.fontLoaded.emit(fontName);
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.fontError.emit({fontName, error: error.message});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme with custom transition
|
||||
*/
|
||||
applyThemeWithTransition(themeName: string, transitionType: 'fade' | 'scale' | 'typewriter' = 'fade'): void {
|
||||
const theme = this.themeService.getTheme(themeName);
|
||||
if (!theme) return;
|
||||
|
||||
const fontVariables = {
|
||||
'--font-family-sans': this.buildFontStack(theme.fonts.sans),
|
||||
'--font-family-serif': this.buildFontStack(theme.fonts.serif),
|
||||
'--font-family-mono': this.buildFontStack(theme.fonts.mono),
|
||||
'--font-family-display': this.buildFontStack(theme.fonts.display)
|
||||
};
|
||||
|
||||
let transition$: Observable<void>;
|
||||
|
||||
switch (transitionType) {
|
||||
case 'scale':
|
||||
transition$ = this.transitionService.applyWithScale(fontVariables);
|
||||
break;
|
||||
case 'typewriter':
|
||||
transition$ = this.transitionService.applyTypewriterTransition(fontVariables);
|
||||
break;
|
||||
default:
|
||||
transition$ = this.transitionService.applyWithFade(fontVariables);
|
||||
}
|
||||
|
||||
transition$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.selectedTheme = themeName;
|
||||
this.themeChanged.emit(themeName);
|
||||
},
|
||||
error: (error) => {
|
||||
this.fontError.emit({fontName: themeName, error: error.message});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private buildFontStack(font: FontDefinition): string {
|
||||
return [font.family, ...font.fallbacks]
|
||||
.map(f => f.includes(' ') ? `"${f}"` : f)
|
||||
.join(', ');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UiFontManagerService } from './ui-font-manager.service';
|
||||
|
||||
describe('UiFontManagerService', () => {
|
||||
let service: UiFontManagerService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(UiFontManagerService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
237
projects/ui-font-manager/src/lib/ui-font-manager.service.ts
Normal file
237
projects/ui-font-manager/src/lib/ui-font-manager.service.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { Injectable, Inject } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { BehaviorSubject, Observable, from, of, forkJoin } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
import { FontDefinition, FontTheme, FontManagerConfig, FontLoadingOptions } from './types/font-types';
|
||||
import { GOOGLE_FONTS_PRESET, FONT_COMBINATIONS } from './presets/google-fonts-preset';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UiFontManagerService {
|
||||
private loadedFonts = new Set<string>();
|
||||
private activeTheme$ = new BehaviorSubject<string>('default');
|
||||
private customPresets = new Map<string, FontDefinition>();
|
||||
|
||||
private config: FontManagerConfig = {
|
||||
defaultTheme: 'default',
|
||||
preloadFonts: false,
|
||||
fallbackTimeout: 3000,
|
||||
cacheEnabled: true
|
||||
};
|
||||
|
||||
constructor(@Inject(DOCUMENT) private document: Document) {}
|
||||
|
||||
/**
|
||||
* Configure the font manager
|
||||
*/
|
||||
configure(config: Partial<FontManagerConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available Google Fonts presets
|
||||
*/
|
||||
getGoogleFontsPresets(): { [key: string]: FontDefinition } {
|
||||
return GOOGLE_FONTS_PRESET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get popular font combinations
|
||||
*/
|
||||
getFontCombinations(): typeof FONT_COMBINATIONS {
|
||||
return FONT_COMBINATIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom font preset
|
||||
*/
|
||||
addCustomFont(name: string, definition: FontDefinition): void {
|
||||
this.customPresets.set(name, definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available fonts (Google Fonts + Custom)
|
||||
*/
|
||||
getAllFonts(): { [key: string]: FontDefinition } {
|
||||
return {
|
||||
...GOOGLE_FONTS_PRESET,
|
||||
...Object.fromEntries(this.customPresets)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a single font dynamically
|
||||
*/
|
||||
loadFont(fontName: string, options?: FontLoadingOptions): Observable<boolean> {
|
||||
const font = this.getAllFonts()[fontName];
|
||||
if (!font) {
|
||||
console.warn(`Font "${fontName}" not found in presets`);
|
||||
return of(false);
|
||||
}
|
||||
|
||||
if (this.loadedFonts.has(fontName)) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
return this.loadFontFromDefinition(font, options).pipe(
|
||||
map(() => {
|
||||
this.loadedFonts.add(fontName);
|
||||
return true;
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error(`Failed to load font "${fontName}":`, error);
|
||||
return of(false);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load multiple fonts
|
||||
*/
|
||||
loadFonts(fontNames: string[], options?: FontLoadingOptions): Observable<boolean[]> {
|
||||
const loadObservables = fontNames.map(name => this.loadFont(name, options));
|
||||
return forkJoin(loadObservables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a font theme
|
||||
*/
|
||||
applyTheme(theme: FontTheme): Observable<boolean> {
|
||||
const fontsToLoad = Object.values(theme.fonts);
|
||||
const fontNames = fontsToLoad.map(font => font.family);
|
||||
|
||||
return this.loadFonts(fontNames).pipe(
|
||||
map(results => {
|
||||
if (results.every(success => success)) {
|
||||
this.updateCSSVariables(theme);
|
||||
this.activeTheme$.next(theme.name);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current active theme
|
||||
*/
|
||||
getActiveTheme(): Observable<string> {
|
||||
return this.activeTheme$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a theme from font combination
|
||||
*/
|
||||
createThemeFromCombination(name: string, combinationName: string): FontTheme | null {
|
||||
const combination = FONT_COMBINATIONS[combinationName as keyof typeof FONT_COMBINATIONS];
|
||||
if (!combination) return null;
|
||||
|
||||
const allFonts = this.getAllFonts();
|
||||
|
||||
return {
|
||||
name,
|
||||
fonts: {
|
||||
sans: allFonts[combination.sans],
|
||||
serif: allFonts[combination.serif],
|
||||
mono: allFonts[combination.mono],
|
||||
display: allFonts[combination.display]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if font is loaded
|
||||
*/
|
||||
isFontLoaded(fontName: string): boolean {
|
||||
return this.loadedFonts.has(fontName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload fonts for performance
|
||||
*/
|
||||
preloadFonts(fontNames: string[]): void {
|
||||
if (!this.config.preloadFonts) return;
|
||||
|
||||
fontNames.forEach(fontName => {
|
||||
const font = this.getAllFonts()[fontName];
|
||||
if (font?.url) {
|
||||
const link = this.document.createElement('link');
|
||||
link.rel = 'preload';
|
||||
link.href = font.url;
|
||||
link.as = 'style';
|
||||
this.document.head.appendChild(link);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private loadFontFromDefinition(font: FontDefinition, options?: FontLoadingOptions): Observable<void> {
|
||||
return new Observable(observer => {
|
||||
if (!font.url) {
|
||||
// Assume system font or already available
|
||||
observer.next();
|
||||
observer.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already loaded
|
||||
const existingLink = this.document.querySelector(`link[href="${font.url}"]`);
|
||||
if (existingLink) {
|
||||
observer.next();
|
||||
observer.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and load font
|
||||
const link = this.document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = this.buildFontURL(font, options);
|
||||
|
||||
link.onload = () => {
|
||||
observer.next();
|
||||
observer.complete();
|
||||
};
|
||||
|
||||
link.onerror = (error) => {
|
||||
observer.error(error);
|
||||
};
|
||||
|
||||
this.document.head.appendChild(link);
|
||||
|
||||
// Fallback timeout
|
||||
setTimeout(() => {
|
||||
if (!observer.closed) {
|
||||
observer.error(new Error('Font loading timeout'));
|
||||
}
|
||||
}, this.config.fallbackTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
private buildFontURL(font: FontDefinition, options?: FontLoadingOptions): string {
|
||||
if (!font.url) return '';
|
||||
|
||||
let url = font.url;
|
||||
|
||||
// Add display parameter for Google Fonts
|
||||
if (url.includes('fonts.googleapis.com') && options?.display) {
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
url += `${separator}display=${options.display}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
private updateCSSVariables(theme: FontTheme): void {
|
||||
const root = this.document.documentElement;
|
||||
|
||||
// Update CSS custom properties
|
||||
root.style.setProperty('--font-family-sans', this.buildFontStack(theme.fonts.sans));
|
||||
root.style.setProperty('--font-family-serif', this.buildFontStack(theme.fonts.serif));
|
||||
root.style.setProperty('--font-family-mono', this.buildFontStack(theme.fonts.mono));
|
||||
root.style.setProperty('--font-family-display', this.buildFontStack(theme.fonts.display));
|
||||
}
|
||||
|
||||
private buildFontStack(font: FontDefinition): string {
|
||||
return [font.family, ...font.fallbacks].map(f => f.includes(' ') ? `"${f}"` : f).join(', ');
|
||||
}
|
||||
}
|
||||
12
projects/ui-font-manager/src/public-api.ts
Normal file
12
projects/ui-font-manager/src/public-api.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Public API Surface of ui-font-manager
|
||||
*/
|
||||
|
||||
export * from './lib/ui-font-manager.service';
|
||||
export * from './lib/ui-font-manager.component';
|
||||
export * from './lib/services/font-theme.service';
|
||||
export * from './lib/services/font-transition.service';
|
||||
export * from './lib/services/font-loading-state.service';
|
||||
export * from './lib/types/font-types';
|
||||
export * from './lib/presets/google-fonts-preset';
|
||||
export * from './lib/presets/font-preset-manager';
|
||||
Reference in New Issue
Block a user