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:
skyai_dev
2025-09-05 05:37:37 +10:00
parent 876eb301a0
commit 5346d6d0c9
5476 changed files with 350855 additions and 10 deletions

View 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();

View 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'
}
};

View File

@@ -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
});
}
}

View 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);
}
}

View File

@@ -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
});
});
}
}

View 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;
}

View File

@@ -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();
});
});

View 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(', ');
}
}

View File

@@ -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();
});
});

View 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(', ');
}
}

View 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';