feat(text): integrate Google Fonts API with dynamic loading

Added Google Fonts support to text tool:

Font Loader System:
- Created GoogleFontsLoader class with caching and loading states
- Singleton instance with preloading of popular fonts (Roboto, Open Sans, Lato, Montserrat)
- Handles font loading via Google Fonts API with error handling
- Tracks loaded, loading, and error states per font

UI Improvements:
- Updated font selector with optgroups (Web Safe Fonts vs Google Fonts)
- 13 web-safe fonts + 14 popular Google Fonts
- Font preview in dropdown (fontFamily style applied to options)
- Async loading on font selection with error handling

Features:
- 27 total fonts available (13 web-safe + 14 Google Fonts)
- Automatic preloading of 4 most popular fonts on app start
- Font caching to avoid redundant loads
- Fallback to web-safe fonts if Google Fonts fail to load

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-21 16:44:42 +01:00
parent 517f57126a
commit 041db0aaff
2 changed files with 184 additions and 17 deletions

View File

@@ -4,6 +4,9 @@ import { useToolStore } from '@/store';
import { useShapeStore } from '@/store/shape-store';
import { useSelectionStore } from '@/store/selection-store';
import { useTextStore } from '@/store/text-store';
import { WEB_SAFE_FONTS, GOOGLE_FONTS } from '@/lib/text-utils';
import { googleFontsLoader } from '@/lib/google-fonts-loader';
import { useCallback } from 'react';
export function ToolOptions() {
const { activeTool, settings, setSize, setOpacity, setHardness, setColor, setFlow } = useToolStore();
@@ -21,6 +24,22 @@ export function ToolOptions() {
setColor: setTextColor,
} = useTextStore();
// Handle font change with Google Fonts loading
const handleFontChange = useCallback(
async (fontFamily: string) => {
// Check if it's a Google Font
if (GOOGLE_FONTS.includes(fontFamily as any)) {
try {
await googleFontsLoader.loadFont(fontFamily);
} catch (error) {
console.error('Failed to load Google Font:', error);
}
}
setFontFamily(fontFamily);
},
[setFontFamily]
);
// Drawing tools: brush, pencil, eraser
const isDrawingTool = ['brush', 'eraser', 'pencil'].includes(activeTool);
const showHardness = ['brush'].includes(activeTool);
@@ -233,25 +252,24 @@ export function ToolOptions() {
</label>
<select
value={textSettings.fontFamily}
onChange={(e) => setFontFamily(e.target.value)}
onChange={(e) => handleFontChange(e.target.value)}
className="px-3 py-1.5 text-sm rounded-md border border-border bg-background text-foreground"
style={{ fontFamily: textSettings.fontFamily }}
>
<option value="Arial">Arial</option>
<option value="Helvetica">Helvetica</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Georgia">Georgia</option>
<option value="Courier New">Courier New</option>
<option value="Verdana">Verdana</option>
<option value="Trebuchet MS">Trebuchet MS</option>
<option value="Impact">Impact</option>
<option value="Comic Sans MS">Comic Sans MS</option>
<option value="Palatino">Palatino</option>
<option value="Garamond">Garamond</option>
<option value="Bookman">Bookman</option>
<option value="Tahoma">Tahoma</option>
<option value="Lucida Console">Lucida Console</option>
<option value="Monaco">Monaco</option>
<option value="Consolas">Consolas</option>
<optgroup label="Web Safe Fonts">
{WEB_SAFE_FONTS.map((font) => (
<option key={font} value={font} style={{ fontFamily: font }}>
{font}
</option>
))}
</optgroup>
<optgroup label="Google Fonts">
{GOOGLE_FONTS.map((font) => (
<option key={font} value={font} style={{ fontFamily: font }}>
{font}
</option>
))}
</optgroup>
</select>
</div>

149
lib/google-fonts-loader.ts Normal file
View File

@@ -0,0 +1,149 @@
/**
* Google Fonts Loader
* Handles dynamic loading and caching of Google Fonts
*/
export interface FontLoadStatus {
loaded: boolean;
loading: boolean;
error: string | null;
}
class GoogleFontsLoader {
private loadedFonts = new Set<string>();
private loadingFonts = new Map<string, Promise<void>>();
private fontStatuses = new Map<string, FontLoadStatus>();
/**
* Load a Google Font dynamically
*/
async loadFont(fontFamily: string): Promise<void> {
// Already loaded
if (this.loadedFonts.has(fontFamily)) {
return Promise.resolve();
}
// Currently loading
if (this.loadingFonts.has(fontFamily)) {
return this.loadingFonts.get(fontFamily)!;
}
// Start loading
this.fontStatuses.set(fontFamily, {
loaded: false,
loading: true,
error: null,
});
const loadPromise = this.loadFontImpl(fontFamily);
this.loadingFonts.set(fontFamily, loadPromise);
try {
await loadPromise;
this.loadedFonts.add(fontFamily);
this.fontStatuses.set(fontFamily, {
loaded: true,
loading: false,
error: null,
});
} catch (error) {
this.fontStatuses.set(fontFamily, {
loaded: false,
loading: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
throw error;
} finally {
this.loadingFonts.delete(fontFamily);
}
}
/**
* Implementation of font loading
*/
private async loadFontImpl(fontFamily: string): Promise<void> {
return new Promise((resolve, reject) => {
// Check if font is already loaded in browser
if (document.fonts.check(`16px "${fontFamily}"`)) {
resolve();
return;
}
// Create link element to load font
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(
/ /g,
'+'
)}:wght@100;300;400;500;700;900&display=swap`;
link.onload = () => {
// Wait for font to be ready
document.fonts
.load(`16px "${fontFamily}"`)
.then(() => {
resolve();
})
.catch(reject);
};
link.onerror = () => {
reject(new Error(`Failed to load font: ${fontFamily}`));
};
document.head.appendChild(link);
});
}
/**
* Preload multiple fonts
*/
async preloadFonts(fontFamilies: string[]): Promise<void> {
await Promise.allSettled(fontFamilies.map((font) => this.loadFont(font)));
}
/**
* Get font loading status
*/
getStatus(fontFamily: string): FontLoadStatus {
return (
this.fontStatuses.get(fontFamily) || {
loaded: false,
loading: false,
error: null,
}
);
}
/**
* Check if font is loaded
*/
isLoaded(fontFamily: string): boolean {
return this.loadedFonts.has(fontFamily);
}
/**
* Check if font is currently loading
*/
isLoading(fontFamily: string): boolean {
return this.loadingFonts.has(fontFamily);
}
/**
* Clear cache (for testing)
*/
clearCache(): void {
this.loadedFonts.clear();
this.loadingFonts.clear();
this.fontStatuses.clear();
}
}
// Singleton instance
export const googleFontsLoader = new GoogleFontsLoader();
// Preload popular fonts on app start
if (typeof window !== 'undefined') {
// Preload most popular fonts
googleFontsLoader.preloadFonts(['Roboto', 'Open Sans', 'Lato', 'Montserrat']);
}