Files
paint-ui/components/editor/tool-options.tsx
Sebastian Krüger 041db0aaff 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>
2025-11-21 16:44:42 +01:00

412 lines
14 KiB
TypeScript

'use client';
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();
const { settings: shapeSettings, setShapeType } = useShapeStore();
const { selectionType, setSelectionType } = useSelectionStore();
const {
settings: textSettings,
setFontFamily,
setFontSize,
setFontStyle,
setFontWeight,
setAlign,
setLineHeight,
setLetterSpacing,
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);
const showColor = ['brush', 'pencil'].includes(activeTool);
const showFlow = ['brush'].includes(activeTool);
// Fill tool
const isFillTool = activeTool === 'fill';
// Shape tool
const isShapeTool = activeTool === 'shape';
// Selection tool
const isSelectionTool = activeTool === 'select';
// Text tool
const isTextTool = activeTool === 'text';
// Don't show options bar if no options available
if (!isDrawingTool && !isFillTool && !isShapeTool && !isSelectionTool && !isTextTool) {
return null;
}
return (
<div className="flex items-center gap-6 px-4 flex-1">
{/* Drawing Tools Options */}
{isDrawingTool && (
<>
{showColor && (
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Color:
</label>
<input
type="color"
value={settings.color}
onChange={(e) => setColor(e.target.value)}
className="h-8 w-16 rounded border border-border cursor-pointer"
/>
<input
type="text"
value={settings.color}
onChange={(e) => setColor(e.target.value)}
className="w-24 px-2 py-1 text-xs rounded border border-border bg-background text-foreground"
/>
</div>
)}
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Size:
</label>
<input
type="range"
min="1"
max="200"
value={settings.size}
onChange={(e) => setSize(Number(e.target.value))}
className="w-32"
/>
<span className="text-sm text-muted-foreground w-12">
{settings.size}px
</span>
</div>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Opacity:
</label>
<input
type="range"
min="0"
max="100"
value={settings.opacity * 100}
onChange={(e) => setOpacity(Number(e.target.value) / 100)}
className="w-32"
/>
<span className="text-sm text-muted-foreground w-10">
{Math.round(settings.opacity * 100)}%
</span>
</div>
{showHardness && (
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Hardness:
</label>
<input
type="range"
min="0"
max="100"
value={settings.hardness * 100}
onChange={(e) => setHardness(Number(e.target.value) / 100)}
className="w-32"
/>
<span className="text-sm text-muted-foreground w-10">
{Math.round(settings.hardness * 100)}%
</span>
</div>
)}
{showFlow && (
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Flow:
</label>
<input
type="range"
min="0"
max="100"
value={settings.flow * 100}
onChange={(e) => setFlow(Number(e.target.value) / 100)}
className="w-32"
/>
<span className="text-sm text-muted-foreground w-10">
{Math.round(settings.flow * 100)}%
</span>
</div>
)}
</>
)}
{/* Fill Tool Options */}
{isFillTool && (
<>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Color:
</label>
<input
type="color"
value={settings.color}
onChange={(e) => setColor(e.target.value)}
className="h-8 w-16 rounded border border-border cursor-pointer"
/>
<input
type="text"
value={settings.color}
onChange={(e) => setColor(e.target.value)}
className="w-24 px-2 py-1 text-xs rounded border border-border bg-background text-foreground"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Opacity:
</label>
<input
type="range"
min="0"
max="100"
value={settings.opacity * 100}
onChange={(e) => setOpacity(Number(e.target.value) / 100)}
className="w-32"
/>
<span className="text-sm text-muted-foreground w-10">
{Math.round(settings.opacity * 100)}%
</span>
</div>
</>
)}
{/* Shape Tool Options */}
{isShapeTool && (
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Shape:
</label>
<select
value={shapeSettings.type}
onChange={(e) => setShapeType(e.target.value as any)}
className="px-3 py-1.5 text-sm rounded-md border border-border bg-background text-foreground"
>
<option value="rectangle">Rectangle</option>
<option value="ellipse">Ellipse</option>
<option value="line">Line</option>
<option value="arrow">Arrow</option>
<option value="polygon">Polygon</option>
<option value="star">Star</option>
<option value="triangle">Triangle</option>
</select>
</div>
)}
{/* Selection Tool Options */}
{isSelectionTool && (
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Mode:
</label>
<select
value={selectionType}
onChange={(e) => setSelectionType(e.target.value as any)}
className="px-3 py-1.5 text-sm rounded-md border border-border bg-background text-foreground"
>
<option value="rectangular">Rectangular</option>
<option value="elliptical">Elliptical</option>
<option value="lasso">Lasso</option>
<option value="magic-wand">Magic Wand</option>
</select>
</div>
)}
{/* Text Tool Options */}
{isTextTool && (
<>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Font:
</label>
<select
value={textSettings.fontFamily}
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 }}
>
<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>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Size:
</label>
<input
type="number"
min="8"
max="500"
value={textSettings.fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
className="w-20 px-2 py-1 text-sm rounded border border-border bg-background text-foreground"
/>
<span className="text-sm text-muted-foreground">px</span>
</div>
{/* Font Style: Bold/Italic */}
<div className="flex items-center gap-1">
<button
onClick={() => setFontWeight(textSettings.fontWeight === 'bold' ? 'normal' : 'bold')}
className={`px-3 py-1.5 text-sm font-bold rounded-md border transition-colors ${
textSettings.fontWeight === 'bold'
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background text-foreground border-border hover:bg-accent'
}`}
title="Bold"
>
B
</button>
<button
onClick={() => setFontStyle(textSettings.fontStyle === 'italic' ? 'normal' : 'italic')}
className={`px-3 py-1.5 text-sm italic rounded-md border transition-colors ${
textSettings.fontStyle === 'italic'
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background text-foreground border-border hover:bg-accent'
}`}
title="Italic"
>
I
</button>
</div>
{/* Text Alignment */}
<div className="flex items-center gap-1">
<button
onClick={() => setAlign('left')}
className={`px-3 py-1.5 text-sm rounded-md border transition-colors ${
textSettings.align === 'left'
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background text-foreground border-border hover:bg-accent'
}`}
title="Align Left"
>
</button>
<button
onClick={() => setAlign('center')}
className={`px-3 py-1.5 text-sm rounded-md border transition-colors ${
textSettings.align === 'center'
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background text-foreground border-border hover:bg-accent'
}`}
title="Align Center"
>
</button>
<button
onClick={() => setAlign('right')}
className={`px-3 py-1.5 text-sm rounded-md border transition-colors ${
textSettings.align === 'right'
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background text-foreground border-border hover:bg-accent'
}`}
title="Align Right"
>
</button>
</div>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Color:
</label>
<input
type="color"
value={textSettings.color}
onChange={(e) => setTextColor(e.target.value)}
className="h-8 w-16 rounded border border-border cursor-pointer"
/>
<input
type="text"
value={textSettings.color}
onChange={(e) => setTextColor(e.target.value)}
className="w-24 px-2 py-1 text-xs rounded border border-border bg-background text-foreground"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Line Height:
</label>
<input
type="range"
min="0.5"
max="3"
step="0.1"
value={textSettings.lineHeight}
onChange={(e) => setLineHeight(Number(e.target.value))}
className="w-24"
/>
<span className="text-sm text-muted-foreground w-8">
{textSettings.lineHeight.toFixed(1)}
</span>
</div>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Letter Spacing:
</label>
<input
type="range"
min="-10"
max="50"
step="1"
value={textSettings.letterSpacing}
onChange={(e) => setLetterSpacing(Number(e.target.value))}
className="w-24"
/>
<span className="text-sm text-muted-foreground w-10">
{textSettings.letterSpacing}px
</span>
</div>
</>
)}
</div>
);
}