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:
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(', ');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user