Files
memefast/resources/js/modules/editor/partials/text-sidebar.jsx
2025-06-17 21:59:36 +08:00

375 lines
17 KiB
JavaScript

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Textarea } from '@/components/ui/textarea';
import { useMitt } from '@/plugins/MittContext';
import useVideoEditorStore from '@/stores/VideoEditorStore';
import { Bold, Italic, Minus, Plus, Type } from 'lucide-react';
import { useEffect, useState } from 'react';
// Font configuration - extensible for adding more fonts
const AVAILABLE_FONTS = [
{
name: 'Montserrat',
value: 'Montserrat',
fontFiles: {
normal: '/fonts/Montserrat/static/Montserrat-Regular.ttf',
bold: '/fonts/Montserrat/static/Montserrat-Bold.ttf',
italic: '/fonts/Montserrat/static/Montserrat-Italic.ttf',
boldItalic: '/fonts/Montserrat/static/Montserrat-BoldItalic.ttf',
},
},
];
export default function TextSidebar({ isOpen, onClose }) {
const { selectedTextElement } = useVideoEditorStore();
const emitter = useMitt();
const [textValue, setTextValue] = useState('');
const [fontSize, setFontSize] = useState(24);
const [isBold, setIsBold] = useState(true);
const [isItalic, setIsItalic] = useState(false);
const [fontFamily, setFontFamily] = useState('Montserrat');
const [fillColor, setFillColor] = useState('#ffffff');
const [strokeColor, setStrokeColor] = useState('#000000');
const [strokeWidth, setStrokeWidth] = useState(2);
// Font size constraints
const MIN_FONT_SIZE = 8;
const MAX_FONT_SIZE = 120;
const FONT_SIZE_STEP = 2;
// Stroke width constraints
const MIN_STROKE_WIDTH = 0;
const MAX_STROKE_WIDTH = 3;
const STROKE_WIDTH_STEP = 1;
// Update state when selected element changes - THIS KEEPS SIDEBAR IN SYNC WITH TRANSFORMER
useEffect(() => {
if (selectedTextElement) {
setTextValue(selectedTextElement.text || '');
setFontSize(selectedTextElement.fontSize || 24);
setIsBold(selectedTextElement.fontWeight === 'bold' || selectedTextElement.fontWeight === 700 || true);
setIsItalic(selectedTextElement.fontStyle === 'italic' || false);
setFontFamily(selectedTextElement.fontFamily || 'Montserrat');
setFillColor(selectedTextElement.fill || '#ffffff');
setStrokeColor(selectedTextElement.stroke || '#000000');
setStrokeWidth(selectedTextElement.strokeWidth || 2);
}
}, [selectedTextElement]);
// Handle text changes
const handleTextChange = (e) => {
const newText = e.target.value;
setTextValue(newText);
if (selectedTextElement) {
emitter.emit('text-update', {
elementId: selectedTextElement.id,
updates: { text: newText },
});
}
};
// Handle font size changes - CLAMP AND UPDATE
const handleFontSizeChange = (newSize) => {
const clampedSize = Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, newSize));
setFontSize(clampedSize);
if (selectedTextElement) {
emitter.emit('text-update', {
elementId: selectedTextElement.id,
updates: { fontSize: clampedSize },
});
}
};
// Handle stroke width changes
const handleStrokeWidthChange = (newWidth) => {
const clampedWidth = Math.max(MIN_STROKE_WIDTH, Math.min(MAX_STROKE_WIDTH, newWidth));
setStrokeWidth(clampedWidth);
if (selectedTextElement) {
emitter.emit('text-update', {
elementId: selectedTextElement.id,
updates: { strokeWidth: clampedWidth },
});
}
};
// Handle fill color changes
const handleFillColorChange = (e) => {
const newColor = e.target.value;
setFillColor(newColor);
if (selectedTextElement) {
emitter.emit('text-update', {
elementId: selectedTextElement.id,
updates: { fill: newColor },
});
}
};
// Handle stroke color changes
const handleStrokeColorChange = (e) => {
const newColor = e.target.value;
setStrokeColor(newColor);
if (selectedTextElement) {
emitter.emit('text-update', {
elementId: selectedTextElement.id,
updates: { stroke: newColor },
});
}
};
// Handle font family changes
const handleFontFamilyChange = (newFontFamily) => {
setFontFamily(newFontFamily);
if (selectedTextElement) {
emitter.emit('text-update', {
elementId: selectedTextElement.id,
updates: { fontFamily: newFontFamily },
});
}
};
// Handle bold toggle
const handleBoldToggle = () => {
const newBoldState = !isBold;
setIsBold(newBoldState);
if (selectedTextElement) {
emitter.emit('text-update', {
elementId: selectedTextElement.id,
updates: { fontWeight: newBoldState ? 'bold' : 'normal' },
});
}
};
// Handle italic toggle
const handleItalicToggle = () => {
const newItalicState = !isItalic;
setIsItalic(newItalicState);
if (selectedTextElement) {
emitter.emit('text-update', {
elementId: selectedTextElement.id,
updates: { fontStyle: newItalicState ? 'italic' : 'normal' },
});
}
};
// Increase font size
const increaseFontSize = () => {
handleFontSizeChange(fontSize + FONT_SIZE_STEP);
};
// Decrease font size
const decreaseFontSize = () => {
handleFontSizeChange(fontSize - FONT_SIZE_STEP);
};
// Increase stroke width
const increaseStrokeWidth = () => {
handleStrokeWidthChange(strokeWidth + STROKE_WIDTH_STEP);
};
// Decrease stroke width
const decreaseStrokeWidth = () => {
handleStrokeWidthChange(strokeWidth - STROKE_WIDTH_STEP);
};
return (
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose()}>
<SheetContent side="right" className="max-[140px] w-full overflow-y-auto dark:bg-neutral-900">
<SheetHeader>
<SheetTitle className="flex items-center gap-3">
<Type className="h-6 w-6" />
Edit Text
</SheetTitle>
</SheetHeader>
<div className="mt-6 space-y-4 px-2">
{selectedTextElement ? (
<>
{/* Text Content */}
<div>
<label className="text-sm font-medium">Edit your text here</label>
<Textarea
value={textValue}
onChange={handleTextChange}
placeholder="Enter your text..."
className="mt-2 text-center text-nowrap dark:bg-neutral-800"
rows={4}
style={{
fontFamily: fontFamily,
fontSize: `${fontSize * 0.45}px`, // Cap preview size for readability
fontWeight: isBold ? 'bold' : 'normal',
fontStyle: isItalic ? 'italic' : 'normal',
color: fillColor,
textShadow:
strokeWidth > 0
? `
-${strokeWidth * 0.6}px -${strokeWidth * 0.6}px 0 ${strokeColor},
${strokeWidth * 0.6}px -${strokeWidth * 0.6}px 0 ${strokeColor},
-${strokeWidth * 0.6}px ${strokeWidth * 0.6}px 0 ${strokeColor},
${strokeWidth * 0.6}px ${strokeWidth * 0.6}px 0 ${strokeColor}
`
: 'none',
}}
/>
</div>
{/* Font Family */}
<div>
<label className="text-sm font-medium">Font Family</label>
<Select value={fontFamily} onValueChange={handleFontFamilyChange}>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Select font" />
</SelectTrigger>
<SelectContent>
{AVAILABLE_FONTS.map((font) => (
<SelectItem key={font.value} value={font.value}>
<span
style={{
fontFamily: font.name,
fontWeight: isBold ? 'bold' : 'normal',
fontStyle: isItalic ? 'italic' : 'normal',
}}
>
{font.name}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Font Size Controls */}
<div>
<label className="text-sm font-medium">Font Size</label>
<div className="mt-2 flex items-center justify-between rounded-lg border p-2">
<Button
variant="default"
size="icon"
onClick={decreaseFontSize}
disabled={fontSize <= MIN_FONT_SIZE}
className="h-8 w-8"
>
<Minus className="h-4 w-4" />
</Button>
<div className="flex items-center gap-2">
<span className="text-lg font-semibold">{fontSize}</span>
<span className="text-sm text-gray-500">px</span>
</div>
<Button
variant="default"
size="icon"
onClick={increaseFontSize}
disabled={fontSize >= MAX_FONT_SIZE}
className="h-8 w-8"
>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* Font Size Range Indicator */}
<div className="mt-1 text-center text-xs text-gray-500">
Size range: {MIN_FONT_SIZE}px - {MAX_FONT_SIZE}px
</div>
{/* Note about transformer resize */}
<div className="mt-1 text-center text-xs text-blue-600">💡 You can also resize by dragging the corners</div>
</div>
{/* Font Style Controls */}
<div>
<label className="text-sm font-medium">Font Style</label>
<div className="mt-2 flex gap-2">
<Button
variant={isBold ? 'default' : 'secondary'}
size=""
onClick={handleBoldToggle}
className="flex flex-1 items-center gap-2"
>
<Bold className="h-4 w-4" />
<span className={isBold ? 'font-bold' : 'font-normal'}>Bold</span>
</Button>
<Button
variant={isItalic ? 'default' : 'secondary'}
size=""
onClick={handleItalicToggle}
className="flex flex-1 items-center gap-2"
>
<Italic className="h-4 w-4" />
<span className={isItalic ? 'italic' : 'not-italic'}>Italic</span>
</Button>
</div>
</div>
{/* Font Color */}
<div>
<label className="text-sm font-medium">Font Color</label>
<div className="mt-2 flex items-center gap-2">
<Input className="h-10 w-20" type="color" value={fillColor} onChange={handleFillColorChange} />
<Input type="text" value={fillColor} onChange={handleFillColorChange} placeholder="#ffffff" />
</div>
</div>
{/* Outline Color */}
<div>
<label className="text-sm font-medium">Outline Color</label>
<div className="mt-2 flex items-center gap-2">
<Input className="h-10 w-20" type="color" value={strokeColor} onChange={handleStrokeColorChange} />
<Input type="text" value={strokeColor} onChange={handleStrokeColorChange} placeholder="#000000" />
</div>
</div>
{/* Outline Thickness */}
<div>
<label className="text-sm font-medium">Outline Thickness</label>
<div className="mt-2 flex items-center justify-between rounded-lg border p-2">
<Button
variant="default"
size="icon"
onClick={decreaseStrokeWidth}
disabled={strokeWidth <= MIN_STROKE_WIDTH}
className="h-8 w-8"
>
<Minus className="h-4 w-4" />
</Button>
<div className="flex items-center gap-2">
<span className="text-lg font-semibold">{strokeWidth}</span>
<span className="text-sm text-gray-500">px</span>
</div>
<Button
variant="default"
size="icon"
onClick={increaseStrokeWidth}
disabled={strokeWidth >= MAX_STROKE_WIDTH}
className="h-8 w-8"
>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
</>
) : (
<div className="text-center text-gray-500">
<Type className="mx-auto h-12 w-12 text-gray-300" />
<p className="mt-2">Select a text element to edit</p>
</div>
)}
</div>
</SheetContent>
</Sheet>
);
}