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