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:
@@ -4,6 +4,9 @@ import { useToolStore } from '@/store';
|
|||||||
import { useShapeStore } from '@/store/shape-store';
|
import { useShapeStore } from '@/store/shape-store';
|
||||||
import { useSelectionStore } from '@/store/selection-store';
|
import { useSelectionStore } from '@/store/selection-store';
|
||||||
import { useTextStore } from '@/store/text-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() {
|
export function ToolOptions() {
|
||||||
const { activeTool, settings, setSize, setOpacity, setHardness, setColor, setFlow } = useToolStore();
|
const { activeTool, settings, setSize, setOpacity, setHardness, setColor, setFlow } = useToolStore();
|
||||||
@@ -21,6 +24,22 @@ export function ToolOptions() {
|
|||||||
setColor: setTextColor,
|
setColor: setTextColor,
|
||||||
} = useTextStore();
|
} = 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
|
// Drawing tools: brush, pencil, eraser
|
||||||
const isDrawingTool = ['brush', 'eraser', 'pencil'].includes(activeTool);
|
const isDrawingTool = ['brush', 'eraser', 'pencil'].includes(activeTool);
|
||||||
const showHardness = ['brush'].includes(activeTool);
|
const showHardness = ['brush'].includes(activeTool);
|
||||||
@@ -233,25 +252,24 @@ export function ToolOptions() {
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={textSettings.fontFamily}
|
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"
|
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>
|
<optgroup label="Web Safe Fonts">
|
||||||
<option value="Helvetica">Helvetica</option>
|
{WEB_SAFE_FONTS.map((font) => (
|
||||||
<option value="Times New Roman">Times New Roman</option>
|
<option key={font} value={font} style={{ fontFamily: font }}>
|
||||||
<option value="Georgia">Georgia</option>
|
{font}
|
||||||
<option value="Courier New">Courier New</option>
|
</option>
|
||||||
<option value="Verdana">Verdana</option>
|
))}
|
||||||
<option value="Trebuchet MS">Trebuchet MS</option>
|
</optgroup>
|
||||||
<option value="Impact">Impact</option>
|
<optgroup label="Google Fonts">
|
||||||
<option value="Comic Sans MS">Comic Sans MS</option>
|
{GOOGLE_FONTS.map((font) => (
|
||||||
<option value="Palatino">Palatino</option>
|
<option key={font} value={font} style={{ fontFamily: font }}>
|
||||||
<option value="Garamond">Garamond</option>
|
{font}
|
||||||
<option value="Bookman">Bookman</option>
|
</option>
|
||||||
<option value="Tahoma">Tahoma</option>
|
))}
|
||||||
<option value="Lucida Console">Lucida Console</option>
|
</optgroup>
|
||||||
<option value="Monaco">Monaco</option>
|
|
||||||
<option value="Consolas">Consolas</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
149
lib/google-fonts-loader.ts
Normal file
149
lib/google-fonts-loader.ts
Normal 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']);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user