- 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>
238 lines
6.4 KiB
TypeScript
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(', ');
|
|
}
|
|
}
|