Files
ui-essentials/projects/ui-font-manager/src/lib/ui-font-manager.service.ts
skyai_dev 5346d6d0c9 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>
2025-09-05 05:37:37 +10:00

238 lines
6.4 KiB
TypeScript

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