Update
This commit is contained in:
@@ -4,7 +4,7 @@ import { createInertiaApp } from '@inertiajs/react';
|
|||||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import DetailedErrorFallback from './components/DetailedErrorFallback'; // Import your component
|
import DetailedErrorFallback from './components/custom/detailed-error-fallback'; // Import your component
|
||||||
import { initializeTheme } from './hooks/use-appearance';
|
import { initializeTheme } from './hooks/use-appearance';
|
||||||
import { AxiosProvider } from './plugins/AxiosContext';
|
import { AxiosProvider } from './plugins/AxiosContext';
|
||||||
import { MittProvider } from './plugins/MittContext';
|
import { MittProvider } from './plugins/MittContext';
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ const sampleTimelineElements = [
|
|||||||
x: 50,
|
x: 50,
|
||||||
y: 600,
|
y: 600,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: 'bold', // ADD THIS LINE
|
fontWeight: 'bold',
|
||||||
|
fontFamily: 'Montserrat',
|
||||||
|
fontStyle: 'normal',
|
||||||
fill: 'white',
|
fill: 'white',
|
||||||
stroke: 'black',
|
stroke: 'black',
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
@@ -76,7 +78,9 @@ const sampleTimelineElements = [
|
|||||||
x: 50,
|
x: 50,
|
||||||
y: 650,
|
y: 650,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: 'bold', // ADD THIS LINE
|
fontWeight: 'bold',
|
||||||
|
fontFamily: 'Montserrat',
|
||||||
|
fontStyle: 'normal',
|
||||||
fill: 'yellow',
|
fill: 'yellow',
|
||||||
stroke: 'red',
|
stroke: 'red',
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const VideoEditor = ({ width, height }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// NEW: Handle element transformations (position, scale, rotation)
|
// Handle element transformations (position, scale, rotation) and text properties
|
||||||
const handleElementUpdate = useCallback(
|
const handleElementUpdate = useCallback(
|
||||||
(elementId, updates) => {
|
(elementId, updates) => {
|
||||||
setTimelineElements((prev) =>
|
setTimelineElements((prev) =>
|
||||||
@@ -553,7 +553,7 @@ const VideoEditor = ({ width, height }) => {
|
|||||||
handleSeek={handleSeek}
|
handleSeek={handleSeek}
|
||||||
copyFFmpegCommand={copyFFmpegCommand}
|
copyFFmpegCommand={copyFFmpegCommand}
|
||||||
exportVideo={exportVideo}
|
exportVideo={exportVideo}
|
||||||
onElementUpdate={handleElementUpdate} // NEW: Pass the update handler
|
onElementUpdate={handleElementUpdate}
|
||||||
layerRef={layerRef}
|
layerRef={layerRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,22 @@ import { FFmpeg } from '@ffmpeg/ffmpeg';
|
|||||||
import { fetchFile, toBlobURL } from '@ffmpeg/util';
|
import { fetchFile, toBlobURL } from '@ffmpeg/util';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
// Font configuration mapping
|
||||||
|
const FONT_CONFIG = {
|
||||||
|
Montserrat: {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
Arial: {
|
||||||
|
normal: '/arial.ttf',
|
||||||
|
bold: '/arial.ttf',
|
||||||
|
italic: '/arial.ttf',
|
||||||
|
boldItalic: '/arial.ttf',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||||
const [showConsoleLogs] = useState(false);
|
const [showConsoleLogs] = useState(false);
|
||||||
|
|
||||||
@@ -9,6 +25,25 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
|||||||
const [exportProgress, setExportProgress] = useState(0);
|
const [exportProgress, setExportProgress] = useState(0);
|
||||||
const [exportStatus, setExportStatus] = useState('');
|
const [exportStatus, setExportStatus] = useState('');
|
||||||
|
|
||||||
|
// Helper function to get font file path based on font family and style
|
||||||
|
const getFontFilePath = (fontFamily, fontWeight, fontStyle) => {
|
||||||
|
const family = fontFamily || 'Arial';
|
||||||
|
const config = FONT_CONFIG[family] || FONT_CONFIG.Arial;
|
||||||
|
|
||||||
|
const isBold = fontWeight === 'bold' || fontWeight === 700;
|
||||||
|
const isItalic = fontStyle === 'italic';
|
||||||
|
|
||||||
|
if (isBold && isItalic) {
|
||||||
|
return config.boldItalic;
|
||||||
|
} else if (isBold) {
|
||||||
|
return config.bold;
|
||||||
|
} else if (isItalic) {
|
||||||
|
return config.italic;
|
||||||
|
} else {
|
||||||
|
return config.normal;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const generateFFmpegCommand = useCallback(
|
const generateFFmpegCommand = useCallback(
|
||||||
(is_string = true, useLocalFiles = false) => {
|
(is_string = true, useLocalFiles = false) => {
|
||||||
showConsoleLogs && console.log('🎬 STARTING FFmpeg generation');
|
showConsoleLogs && console.log('🎬 STARTING FFmpeg generation');
|
||||||
@@ -19,6 +54,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
|||||||
|
|
||||||
showConsoleLogs && console.log('Videos found:', videos.length);
|
showConsoleLogs && console.log('Videos found:', videos.length);
|
||||||
showConsoleLogs && console.log('Images found:', images.length);
|
showConsoleLogs && console.log('Images found:', images.length);
|
||||||
|
showConsoleLogs && console.log('Texts found:', texts.length);
|
||||||
|
|
||||||
if (videos.length === 0 && images.length === 0) {
|
if (videos.length === 0 && images.length === 0) {
|
||||||
if (is_string) {
|
if (is_string) {
|
||||||
@@ -98,16 +134,20 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
|||||||
|
|
||||||
showConsoleLogs && console.log('🎵 Audio args:', audioArgs);
|
showConsoleLogs && console.log('🎵 Audio args:', audioArgs);
|
||||||
|
|
||||||
// Process text elements with centering
|
// Process text elements with proper font support
|
||||||
texts.forEach((t, i) => {
|
texts.forEach((t, i) => {
|
||||||
const escapedText = t.text.replace(/'/g, is_string ? "\\'" : "'").replace(/:/g, '\\:');
|
const escapedText = t.text.replace(/'/g, is_string ? "\\'" : "'").replace(/:/g, '\\:');
|
||||||
|
|
||||||
|
// Get the appropriate font file path
|
||||||
|
const fontFilePath = getFontFilePath(t.fontFamily, t.fontWeight, t.fontStyle);
|
||||||
|
const fontFileName = fontFilePath.split('/').pop();
|
||||||
|
|
||||||
// Center the text: x position is the center point, y is adjusted for baseline
|
// Center the text: x position is the center point, y is adjusted for baseline
|
||||||
const centerX = Math.round(t.x);
|
const centerX = Math.round(t.x);
|
||||||
const centerY = Math.round(t.y + t.fontSize * 0.3); // Adjust for text baseline
|
const centerY = Math.round(t.y + t.fontSize * 0.3); // Adjust for text baseline
|
||||||
|
|
||||||
filters.push(
|
filters.push(
|
||||||
`[${videoLayer}]drawtext=fontfile=/arial.ttf:text='${escapedText}':x=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${
|
`[${videoLayer}]drawtext=fontfile=/${fontFileName}:text='${escapedText}':x=${centerX}:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${
|
||||||
t.stroke
|
t.stroke
|
||||||
}:text_align=center:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`,
|
}:text_align=center:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`,
|
||||||
);
|
);
|
||||||
@@ -137,7 +177,6 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
|||||||
|
|
||||||
if (is_string) {
|
if (is_string) {
|
||||||
let inputStrings = [];
|
let inputStrings = [];
|
||||||
let inputIdx = 0;
|
|
||||||
|
|
||||||
videos.forEach((v, i) => {
|
videos.forEach((v, i) => {
|
||||||
inputStrings.push(`-i "${useLocalFiles ? `input_video_${i}.webm` : v.source_webm}"`);
|
inputStrings.push(`-i "${useLocalFiles ? `input_video_${i}.webm` : v.source_webm}"`);
|
||||||
@@ -209,12 +248,49 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
|||||||
wasmURL: wasmBlobURL,
|
wasmURL: wasmBlobURL,
|
||||||
});
|
});
|
||||||
showConsoleLogs && console.log('FFmpeg loaded!');
|
showConsoleLogs && console.log('FFmpeg loaded!');
|
||||||
setExportProgress(20);
|
setExportProgress(10);
|
||||||
|
|
||||||
setExportStatus('Loading font...');
|
setExportStatus('Loading fonts...');
|
||||||
await ffmpeg.writeFile('arial.ttf', await fetchFile('https://raw.githubusercontent.com/ffmpegwasm/testdata/master/arial.ttf'));
|
|
||||||
showConsoleLogs && console.log('Font loaded!');
|
// Load all required fonts
|
||||||
setExportProgress(30);
|
const fontsToLoad = new Set();
|
||||||
|
|
||||||
|
// Add Arial font (fallback)
|
||||||
|
fontsToLoad.add('arial.ttf');
|
||||||
|
|
||||||
|
// Add fonts used by text elements
|
||||||
|
timelineElements
|
||||||
|
.filter((el) => el.type === 'text')
|
||||||
|
.forEach((text) => {
|
||||||
|
const fontFilePath = getFontFilePath(text.fontFamily, text.fontWeight, text.fontStyle);
|
||||||
|
const fontFileName = fontFilePath.split('/').pop();
|
||||||
|
fontsToLoad.add(fontFileName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load each unique font
|
||||||
|
let fontProgress = 0;
|
||||||
|
for (const fontFile of fontsToLoad) {
|
||||||
|
try {
|
||||||
|
if (fontFile === 'arial.ttf') {
|
||||||
|
await ffmpeg.writeFile(
|
||||||
|
'arial.ttf',
|
||||||
|
await fetchFile('https://raw.githubusercontent.com/ffmpegwasm/testdata/master/arial.ttf'),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Load Montserrat fonts from local filesystem
|
||||||
|
const fontPath = `/fonts/Montserrat/static/${fontFile}`;
|
||||||
|
await ffmpeg.writeFile(fontFile, await fetchFile(fontPath));
|
||||||
|
}
|
||||||
|
fontProgress++;
|
||||||
|
setExportProgress(10 + Math.round((fontProgress / fontsToLoad.size) * 10));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to load font ${fontFile}, falling back to arial.ttf:`, error);
|
||||||
|
// If font loading fails, we'll use arial.ttf as fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showConsoleLogs && console.log('Fonts loaded!');
|
||||||
|
setExportProgress(20);
|
||||||
|
|
||||||
setExportStatus('Downloading media...');
|
setExportStatus('Downloading media...');
|
||||||
const videos = timelineElements.filter((el) => el.type === 'video');
|
const videos = timelineElements.filter((el) => el.type === 'video');
|
||||||
@@ -227,14 +303,14 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
|||||||
for (let i = 0; i < videos.length; i++) {
|
for (let i = 0; i < videos.length; i++) {
|
||||||
await ffmpeg.writeFile(`input_video_${i}.webm`, await fetchFile(videos[i].source_webm));
|
await ffmpeg.writeFile(`input_video_${i}.webm`, await fetchFile(videos[i].source_webm));
|
||||||
mediaProgress++;
|
mediaProgress++;
|
||||||
setExportProgress(30 + Math.round((mediaProgress / totalMedia) * 30));
|
setExportProgress(20 + Math.round((mediaProgress / totalMedia) * 40));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download images
|
// Download images
|
||||||
for (let i = 0; i < images.length; i++) {
|
for (let i = 0; i < images.length; i++) {
|
||||||
await ffmpeg.writeFile(`input_image_${i}.jpg`, await fetchFile(images[i].source));
|
await ffmpeg.writeFile(`input_image_${i}.jpg`, await fetchFile(images[i].source));
|
||||||
mediaProgress++;
|
mediaProgress++;
|
||||||
setExportProgress(30 + Math.round((mediaProgress / totalMedia) * 30));
|
setExportProgress(20 + Math.round((mediaProgress / totalMedia) * 40));
|
||||||
}
|
}
|
||||||
|
|
||||||
setExportStatus('Processing video...');
|
setExportStatus('Processing video...');
|
||||||
|
|||||||
@@ -71,6 +71,22 @@ const VideoPreview = ({
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to get font style for text elements
|
||||||
|
const getTextFontStyle = (element) => {
|
||||||
|
const isBold = element.fontWeight === 'bold' || element.fontWeight === 700;
|
||||||
|
const isItalic = element.fontStyle === 'italic';
|
||||||
|
|
||||||
|
if (isBold && isItalic) {
|
||||||
|
return 'bold italic';
|
||||||
|
} else if (isBold) {
|
||||||
|
return 'bold';
|
||||||
|
} else if (isItalic) {
|
||||||
|
return 'italic';
|
||||||
|
} else {
|
||||||
|
return 'normal';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Check if element uses center-offset positioning
|
// Check if element uses center-offset positioning
|
||||||
const usesCenterPositioning = (elementType) => {
|
const usesCenterPositioning = (elementType) => {
|
||||||
return elementType === 'video' || elementType === 'image';
|
return elementType === 'video' || elementType === 'image';
|
||||||
@@ -445,8 +461,8 @@ const VideoPreview = ({
|
|||||||
x={element.x}
|
x={element.x}
|
||||||
y={element.y}
|
y={element.y}
|
||||||
fontSize={element.fontSize}
|
fontSize={element.fontSize}
|
||||||
fontStyle={element.fontWeight === 'bold' || element.fontWeight === 700 ? 'bold' : 'normal'} // ADD THIS LINE
|
fontStyle={getTextFontStyle(element)}
|
||||||
fontFamily="Arial"
|
fontFamily={element.fontFamily || 'Arial'}
|
||||||
fill={element.fill}
|
fill={element.fill}
|
||||||
stroke={element.stroke}
|
stroke={element.stroke}
|
||||||
strokeWidth={element.strokeWidth}
|
strokeWidth={element.strokeWidth}
|
||||||
|
|||||||
@@ -1,29 +1,48 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useMitt } from '@/plugins/MittContext';
|
import { useMitt } from '@/plugins/MittContext';
|
||||||
import useVideoEditorStore from '@/stores/VideoEditorStore';
|
import useVideoEditorStore from '@/stores/VideoEditorStore';
|
||||||
import { Bold, Minus, Plus, Type } from 'lucide-react';
|
import { Bold, Italic, Minus, Plus, Type } from 'lucide-react';
|
||||||
import { useEffect, useState } from '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 }) {
|
export default function TextSidebar({ isOpen, onClose }) {
|
||||||
const { selectedTextElement } = useVideoEditorStore();
|
const { selectedTextElement } = useVideoEditorStore();
|
||||||
const emitter = useMitt();
|
const emitter = useMitt();
|
||||||
const [textValue, setTextValue] = useState('');
|
const [textValue, setTextValue] = useState('');
|
||||||
const [fontSize, setFontSize] = useState(24); // Default font size
|
const [fontSize, setFontSize] = useState(24);
|
||||||
const [isBold, setIsBold] = useState(true); // Default to bold
|
const [isBold, setIsBold] = useState(true);
|
||||||
|
const [isItalic, setIsItalic] = useState(false);
|
||||||
|
const [fontFamily, setFontFamily] = useState('Montserrat');
|
||||||
|
|
||||||
// Font size constraints
|
// Font size constraints
|
||||||
const MIN_FONT_SIZE = 8;
|
const MIN_FONT_SIZE = 8;
|
||||||
const MAX_FONT_SIZE = 120;
|
const MAX_FONT_SIZE = 120;
|
||||||
const FONT_SIZE_STEP = 2;
|
const FONT_SIZE_STEP = 2;
|
||||||
|
|
||||||
// Update textarea, fontSize, and bold when selected element changes
|
// Update state when selected element changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedTextElement) {
|
if (selectedTextElement) {
|
||||||
setTextValue(selectedTextElement.text || '');
|
setTextValue(selectedTextElement.text || '');
|
||||||
setFontSize(selectedTextElement.fontSize || 24);
|
setFontSize(selectedTextElement.fontSize || 24);
|
||||||
setIsBold(selectedTextElement.fontWeight === 'bold' || selectedTextElement.fontWeight === 700 || true); // Default to bold if not set
|
setIsBold(selectedTextElement.fontWeight === 'bold' || selectedTextElement.fontWeight === 700 || true);
|
||||||
|
setIsItalic(selectedTextElement.fontStyle === 'italic' || false);
|
||||||
|
setFontFamily(selectedTextElement.fontFamily || 'Montserrat');
|
||||||
}
|
}
|
||||||
}, [selectedTextElement]);
|
}, [selectedTextElement]);
|
||||||
|
|
||||||
@@ -53,6 +72,18 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle font family changes
|
||||||
|
const handleFontFamilyChange = (newFontFamily) => {
|
||||||
|
setFontFamily(newFontFamily);
|
||||||
|
|
||||||
|
if (selectedTextElement) {
|
||||||
|
emitter.emit('text-update', {
|
||||||
|
elementId: selectedTextElement.id,
|
||||||
|
updates: { fontFamily: newFontFamily },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle bold toggle
|
// Handle bold toggle
|
||||||
const handleBoldToggle = () => {
|
const handleBoldToggle = () => {
|
||||||
const newBoldState = !isBold;
|
const newBoldState = !isBold;
|
||||||
@@ -66,6 +97,19 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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
|
// Increase font size
|
||||||
const increaseFontSize = () => {
|
const increaseFontSize = () => {
|
||||||
handleFontSizeChange(fontSize + FONT_SIZE_STEP);
|
handleFontSizeChange(fontSize + FONT_SIZE_STEP);
|
||||||
@@ -101,6 +145,23 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 }}>{font.name}</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Font Size Controls */}
|
{/* Font Size Controls */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium">Font Size</label>
|
<label className="text-sm font-medium">Font Size</label>
|
||||||
@@ -140,15 +201,25 @@ export default function TextSidebar({ isOpen, onClose }) {
|
|||||||
{/* Font Style Controls */}
|
{/* Font Style Controls */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium">Font Style</label>
|
<label className="text-sm font-medium">Font Style</label>
|
||||||
<div className="mt-2">
|
<div className="mt-2 flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant={isBold ? 'default' : 'outline'}
|
variant={isBold ? 'default' : 'outline'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleBoldToggle}
|
onClick={handleBoldToggle}
|
||||||
className="flex w-full items-center gap-2"
|
className="flex flex-1 items-center gap-2"
|
||||||
>
|
>
|
||||||
<Bold className="h-4 w-4" />
|
<Bold className="h-4 w-4" />
|
||||||
<span className={isBold ? 'font-bold' : 'font-normal'}>{isBold ? 'Bold' : 'Normal'}</span>
|
<span className={isBold ? 'font-bold' : 'font-normal'}>Bold</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={isItalic ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user