289 lines
10 KiB
TypeScript
289 lines
10 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import { X, Type } from 'lucide-react';
|
||
|
|
import { useTextStore } from '@/store/text-store';
|
||
|
|
import { TextTool } from '@/tools/text-tool';
|
||
|
|
import { WEB_SAFE_FONTS, GOOGLE_FONTS, loadGoogleFont } from '@/lib/text-utils';
|
||
|
|
import type { FontStyle, FontWeight, TextAlign } from '@/types/text';
|
||
|
|
|
||
|
|
export function TextDialog() {
|
||
|
|
const {
|
||
|
|
settings,
|
||
|
|
isDialogOpen,
|
||
|
|
clickPosition,
|
||
|
|
setText,
|
||
|
|
setFontFamily,
|
||
|
|
setFontSize,
|
||
|
|
setFontStyle,
|
||
|
|
setFontWeight,
|
||
|
|
setColor,
|
||
|
|
setAlign,
|
||
|
|
setLineHeight,
|
||
|
|
setLetterSpacing,
|
||
|
|
closeDialog,
|
||
|
|
} = useTextStore();
|
||
|
|
|
||
|
|
const [text, setTextLocal] = useState(settings.text);
|
||
|
|
const [isLoadingFont, setIsLoadingFont] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isDialogOpen) {
|
||
|
|
setTextLocal(settings.text);
|
||
|
|
}
|
||
|
|
}, [isDialogOpen, settings.text]);
|
||
|
|
|
||
|
|
if (!isDialogOpen || !clickPosition) return null;
|
||
|
|
|
||
|
|
const handleInsert = async () => {
|
||
|
|
if (!text.trim()) {
|
||
|
|
closeDialog();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update text in store
|
||
|
|
setText(text);
|
||
|
|
|
||
|
|
// Load Google Font if needed
|
||
|
|
if (GOOGLE_FONTS.includes(settings.fontFamily as any)) {
|
||
|
|
try {
|
||
|
|
setIsLoadingFont(true);
|
||
|
|
await loadGoogleFont(settings.fontFamily);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to load font:', error);
|
||
|
|
} finally {
|
||
|
|
setIsLoadingFont(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Render text on canvas
|
||
|
|
TextTool.renderTextOnCanvas(clickPosition.x, clickPosition.y);
|
||
|
|
|
||
|
|
// Close dialog
|
||
|
|
closeDialog();
|
||
|
|
setTextLocal('');
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleCancel = () => {
|
||
|
|
closeDialog();
|
||
|
|
setTextLocal('');
|
||
|
|
};
|
||
|
|
|
||
|
|
const allFonts = [...WEB_SAFE_FONTS, ...GOOGLE_FONTS];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||
|
|
onClick={handleCancel}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className="bg-card border border-border rounded-lg shadow-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto"
|
||
|
|
onClick={(e) => e.stopPropagation()}
|
||
|
|
>
|
||
|
|
{/* Header */}
|
||
|
|
<div className="flex items-center justify-between p-4 border-b border-border sticky top-0 bg-card z-10">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Type className="h-5 w-5 text-primary" />
|
||
|
|
<h2 className="text-lg font-semibold text-card-foreground">Add Text</h2>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={handleCancel}
|
||
|
|
className="p-1 hover:bg-accent rounded transition-colors"
|
||
|
|
>
|
||
|
|
<X className="h-5 w-5" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Content */}
|
||
|
|
<div className="p-4 space-y-4">
|
||
|
|
{/* Text Input */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label className="text-sm font-medium text-card-foreground">Text</label>
|
||
|
|
<textarea
|
||
|
|
value={text}
|
||
|
|
onChange={(e) => setTextLocal(e.target.value)}
|
||
|
|
placeholder="Enter your text here..."
|
||
|
|
className="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground min-h-[120px] resize-y"
|
||
|
|
autoFocus
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Font Family */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label className="text-sm font-medium text-card-foreground">Font Family</label>
|
||
|
|
<select
|
||
|
|
value={settings.fontFamily}
|
||
|
|
onChange={(e) => setFontFamily(e.target.value)}
|
||
|
|
className="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground"
|
||
|
|
>
|
||
|
|
<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}>
|
||
|
|
{font}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</optgroup>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Font Size, Style, Weight */}
|
||
|
|
<div className="grid grid-cols-3 gap-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label className="text-sm font-medium text-card-foreground">Size</label>
|
||
|
|
<input
|
||
|
|
type="number"
|
||
|
|
min="8"
|
||
|
|
max="500"
|
||
|
|
value={settings.fontSize}
|
||
|
|
onChange={(e) => setFontSize(Number(e.target.value))}
|
||
|
|
className="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label className="text-sm font-medium text-card-foreground">Style</label>
|
||
|
|
<select
|
||
|
|
value={settings.fontStyle}
|
||
|
|
onChange={(e) => setFontStyle(e.target.value as FontStyle)}
|
||
|
|
className="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground"
|
||
|
|
>
|
||
|
|
<option value="normal">Normal</option>
|
||
|
|
<option value="italic">Italic</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label className="text-sm font-medium text-card-foreground">Weight</label>
|
||
|
|
<select
|
||
|
|
value={settings.fontWeight}
|
||
|
|
onChange={(e) => setFontWeight(e.target.value as FontWeight)}
|
||
|
|
className="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground"
|
||
|
|
>
|
||
|
|
<option value="normal">Normal</option>
|
||
|
|
<option value="bold">Bold</option>
|
||
|
|
<option value="100">100 - Thin</option>
|
||
|
|
<option value="300">300 - Light</option>
|
||
|
|
<option value="500">500 - Medium</option>
|
||
|
|
<option value="600">600 - Semi Bold</option>
|
||
|
|
<option value="700">700 - Bold</option>
|
||
|
|
<option value="900">900 - Black</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Color and Alignment */}
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label className="text-sm font-medium text-card-foreground">Color</label>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<input
|
||
|
|
type="color"
|
||
|
|
value={settings.color}
|
||
|
|
onChange={(e) => setColor(e.target.value)}
|
||
|
|
className="h-10 w-16 rounded border border-border cursor-pointer"
|
||
|
|
/>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={settings.color}
|
||
|
|
onChange={(e) => setColor(e.target.value)}
|
||
|
|
className="flex-1 px-3 py-2 rounded-md border border-border bg-background text-foreground"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label className="text-sm font-medium text-card-foreground">Alignment</label>
|
||
|
|
<select
|
||
|
|
value={settings.align}
|
||
|
|
onChange={(e) => setAlign(e.target.value as TextAlign)}
|
||
|
|
className="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground"
|
||
|
|
>
|
||
|
|
<option value="left">Left</option>
|
||
|
|
<option value="center">Center</option>
|
||
|
|
<option value="right">Right</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Line Height and Letter Spacing */}
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label className="text-sm font-medium text-card-foreground">
|
||
|
|
Line Height ({settings.lineHeight.toFixed(2)})
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="range"
|
||
|
|
min="0.5"
|
||
|
|
max="3"
|
||
|
|
step="0.1"
|
||
|
|
value={settings.lineHeight}
|
||
|
|
onChange={(e) => setLineHeight(Number(e.target.value))}
|
||
|
|
className="w-full"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label className="text-sm font-medium text-card-foreground">
|
||
|
|
Letter Spacing ({settings.letterSpacing}px)
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="range"
|
||
|
|
min="-10"
|
||
|
|
max="50"
|
||
|
|
step="1"
|
||
|
|
value={settings.letterSpacing}
|
||
|
|
onChange={(e) => setLetterSpacing(Number(e.target.value))}
|
||
|
|
className="w-full"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Preview */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<label className="text-sm font-medium text-card-foreground">Preview</label>
|
||
|
|
<div
|
||
|
|
className="w-full px-4 py-8 rounded-md border border-border bg-background flex items-center justify-center min-h-[100px]"
|
||
|
|
style={{
|
||
|
|
color: settings.color,
|
||
|
|
fontFamily: settings.fontFamily,
|
||
|
|
fontSize: `${Math.min(settings.fontSize, 48)}px`,
|
||
|
|
fontStyle: settings.fontStyle,
|
||
|
|
fontWeight: settings.fontWeight,
|
||
|
|
textAlign: settings.align,
|
||
|
|
lineHeight: settings.lineHeight,
|
||
|
|
letterSpacing: `${settings.letterSpacing}px`,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{text || 'Your text will appear here'}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Footer */}
|
||
|
|
<div className="flex items-center justify-end gap-2 p-4 border-t border-border sticky bottom-0 bg-card">
|
||
|
|
<button
|
||
|
|
onClick={handleCancel}
|
||
|
|
className="px-4 py-2 rounded-md border border-border hover:bg-accent transition-colors"
|
||
|
|
>
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={handleInsert}
|
||
|
|
disabled={!text.trim() || isLoadingFont}
|
||
|
|
className="px-4 py-2 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
|
|
>
|
||
|
|
{isLoadingFont ? 'Loading Font...' : 'Insert Text'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|