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,224 @@
# UI Font Manager
A comprehensive Angular library for dynamic font management with runtime theme switching, Google Fonts integration, and extensible custom font support.
## Features
- **🎨 Runtime Font Themes**: Switch font themes dynamically without rebuilds
- **📦 Google Fonts Presets**: 75+ popular Google Fonts with URLs ready to use
- **🔧 Custom Font Support**: Add Typekit, self-hosted, or any external fonts
- **⚡ Performance Optimized**: Lazy loading, preloading, and caching strategies
- **🎯 Theme Management**: Create, save, and apply font theme combinations
- **📱 CSS Variables Integration**: Seamless integration with your design system
## Installation
```bash
npm install ui-font-manager
```
## Quick Start
### 1. Basic Usage
```typescript
import { UiFontManagerService, FontThemeService } from 'ui-font-manager';
@Component({...})
export class MyComponent {
constructor(
private fontManager: UiFontManagerService,
private themeService: FontThemeService
) {}
// Load a Google Font
loadFont() {
this.fontManager.loadFont('Inter').subscribe(success => {
console.log('Font loaded:', success);
});
}
// Apply a predefined theme
applyTheme() {
this.themeService.applyTheme('Modern Clean').subscribe();
}
}
```
### 2. CSS Variables Integration
Your CSS automatically uses the new fonts:
```scss
.my-component {
font-family: var(--font-family-sans); // Updates dynamically
}
h1 {
font-family: var(--font-family-display);
}
code {
font-family: var(--font-family-mono);
}
```
## Google Fonts Presets
Access 75+ popular Google Fonts instantly:
```typescript
// Get all available Google Fonts
const fonts = this.fontManager.getGoogleFontsPresets();
// Load specific fonts
this.fontManager.loadFonts(['Inter', 'Playfair Display', 'JetBrains Mono']);
// Use predefined combinations
const combinations = this.fontManager.getFontCombinations();
// 'Modern Clean', 'Classic Editorial', 'Friendly Modern', 'Professional'
```
### Available Google Fonts by Category
**Sans-serif (35 fonts)**: Inter, Roboto, Open Sans, Poppins, Lato, Montserrat, Nunito, Source Sans Pro, Raleway, Ubuntu, Work Sans, Rubik, Kanit, DM Sans, Mulish, Hind, Quicksand, Karla, Manrope, Comfortaa, Outfit, Lexend, Plus Jakarta Sans, Space Grotesk, Figtree, Barlow, Epilogue, Sora, Red Hat Display, Satoshi, Heebo, Exo 2, Commissioner, Archivo, Public Sans
**Serif (20 fonts)**: Playfair Display, Merriweather, Lora, EB Garamond, Crimson Text, Source Serif Pro, Noto Serif, Vollkorn, Libre Baskerville, Arvo, Zilla Slab, Cormorant Garamond, PT Serif, Alegreya, Spectral, Old Standard TT, Cardo, Gentium Plus, Neuton, Bitter
**Monospace (10 fonts)**: JetBrains Mono, Fira Code, Source Code Pro, Roboto Mono, IBM Plex Mono, Space Mono, Inconsolata, PT Mono, Fira Mono, Ubuntu Mono
**Display (10 fonts)**: Oswald, Bebas Neue, Anton, Righteous, Fredoka One, Archivo Black, Cabin, Prompt, Fjalla One, Patua One
**Handwriting (10 fonts)**: Dancing Script, Great Vibes, Pacifico, Kaushan Script, Satisfy, Caveat, Amatic SC, Indie Flower, Shadows Into Light, Permanent Marker
## Custom Font Support
### Add External Fonts
```typescript
import { fontPresetManager } from 'ui-font-manager';
// Add Typekit fonts
fontPresetManager.addTypekitFont('abc123', [
{ name: 'My Custom Font', family: 'MyFont', weights: [400, 700] }
]);
// Add custom CDN fonts
fontPresetManager.addExternalFont('Custom Sans', {
family: 'CustomSans',
url: 'https://mycdn.com/fonts/custom.css',
weights: [300, 400, 600],
category: 'sans-serif'
});
// Add self-hosted fonts
fontPresetManager.addSelfHostedFont('My Font', {
family: 'MyFont',
basePath: '/assets/fonts/myfont',
formats: ['woff2', 'woff'],
weights: [400, 700],
category: 'serif'
});
```
## Theme Management
### Create Custom Themes
```typescript
// Create theme from font combination
const theme = this.themeService.createThemeFromCombination(
'My Theme',
'Modern Clean',
'Clean and modern design'
);
// Create fully custom theme
const customTheme = this.themeService.createCustomTheme('Custom Theme', {
sans: 'Inter',
serif: 'Playfair Display',
mono: 'JetBrains Mono',
display: 'Oswald'
});
// Apply the theme
this.themeService.applyTheme('My Theme').subscribe();
```
### Export/Import Themes
```typescript
// Export theme configuration
const config = this.themeService.exportTheme('My Theme');
// Import theme from JSON
const theme = this.themeService.importTheme(jsonString);
```
## Performance Features
### Font Loading Strategies
```typescript
// Configure font manager
this.fontManager.configure({
preloadFonts: true,
fallbackTimeout: 3000,
cacheEnabled: true
});
// Preload fonts for performance
this.fontManager.preloadFonts(['Inter', 'Roboto']);
// Load with specific options
this.fontManager.loadFont('Inter', {
display: 'swap',
weights: [400, 600],
subsets: ['latin']
});
```
## Integration with Design Systems
Works seamlessly with existing CSS custom properties:
```scss
// In your existing design system
:root {
--font-family-sans: 'Inter', system-ui, sans-serif;
--font-family-serif: 'Playfair Display', Georgia, serif;
--font-family-mono: 'JetBrains Mono', Consolas, monospace;
--font-family-display: 'Inter', system-ui, sans-serif;
}
// Font manager updates these variables at runtime
```
## API Reference
### UiFontManagerService
- `loadFont(name, options?)`: Load single font
- `loadFonts(names, options?)`: Load multiple fonts
- `applyTheme(theme)`: Apply font theme
- `getGoogleFontsPresets()`: Get Google Fonts collection
- `getFontCombinations()`: Get predefined combinations
- `addCustomFont(name, definition)`: Add custom font
### FontThemeService
- `applyTheme(name)`: Apply theme by name
- `getThemes()`: Get all themes
- `createCustomTheme()`: Create custom theme
- `exportTheme()` / `importTheme()`: Export/import themes
### FontPresetManager
- `addCustomFont()`: Add single custom font
- `addTypekitFont()`: Add Adobe Typekit fonts
- `addSelfHostedFont()`: Add self-hosted fonts
- `getFontsByCategory()`: Filter fonts by category
## License
MIT

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/ui-font-manager",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,12 @@
{
"name": "ui-font-manager",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^19.2.0",
"@angular/core": "^19.2.0"
},
"dependencies": {
"tslib": "^2.3.0"
},
"sideEffects": false
}

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

View File

@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": [
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,11 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}