Phase 3: Sidebar & Properties Panel
Implemented full editor UI with three-column layout: - Sidebar with 4 tabs (Upload, Stickers, Text, Templates) - UploadTab with drag-and-drop file upload, wires to POST /api/upload - StickersTab with 96 emoji stickers across 6 categories - TextTab with font picker (20 Google Fonts), size slider, color picker - TemplatesTab placeholder for future template system - LayersPanel showing all elements with select/delete - PropertiesPanel with position, size, rotation controls Also added: - Constants for fonts and stickers - Enhanced CSS with editor-layout, sidebar, properties-panel classes - Updated App.jsx to integrate all components
This commit is contained in:
281
client/src/components/panels/PropertiesPanel.jsx
Normal file
281
client/src/components/panels/PropertiesPanel.jsx
Normal file
@@ -0,0 +1,281 @@
|
||||
export function PropertiesPanel({ element, onUpdate, onDelete }) {
|
||||
if (!element) {
|
||||
return (
|
||||
<div className="properties-panel">
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
borderBottom: `1px solid var(--border)`,
|
||||
}}>
|
||||
<h3 style={{
|
||||
margin: 0,
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: 'var(--text-primary)',
|
||||
}}>
|
||||
Properties
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '1rem',
|
||||
color: 'var(--text-muted)',
|
||||
fontSize: '12px',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
Select an element to edit its properties
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handlePositionChange = (axis, value) => {
|
||||
onUpdate({ [axis]: parseFloat(value) || 0 });
|
||||
};
|
||||
|
||||
const handleSizeChange = (axis, value) => {
|
||||
const numValue = parseFloat(value) || 20;
|
||||
onUpdate({ [axis]: Math.max(20, numValue) });
|
||||
};
|
||||
|
||||
const handleRotationChange = (value) => {
|
||||
const numValue = parseFloat(value) || 0;
|
||||
onUpdate({ rotation: Math.max(-180, Math.min(180, numValue)) });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="properties-panel">
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
borderBottom: `1px solid var(--border)`,
|
||||
}}>
|
||||
<h3 style={{
|
||||
margin: 0,
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: 'var(--text-primary)',
|
||||
}}>
|
||||
Properties
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
padding: '1rem',
|
||||
}}>
|
||||
{/* Element type badge */}
|
||||
<div style={{
|
||||
display: 'inline-block',
|
||||
padding: '4px 8px',
|
||||
background: 'var(--accent-bg)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: 'var(--accent)',
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: '1rem',
|
||||
}}>
|
||||
{element.type}
|
||||
</div>
|
||||
|
||||
{/* Position */}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: 'var(--text-secondary)',
|
||||
marginBottom: '0.5rem',
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
Position
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ fontSize: '10px', color: 'var(--text-muted)' }}>X</label>
|
||||
<input
|
||||
type="number"
|
||||
value={Math.round(element.x)}
|
||||
onChange={(e) => handlePositionChange('x', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.5rem',
|
||||
border: `1px solid var(--border)`,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Y</label>
|
||||
<input
|
||||
type="number"
|
||||
value={Math.round(element.y)}
|
||||
onChange={(e) => handlePositionChange('y', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.5rem',
|
||||
border: `1px solid var(--border)`,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Size (for images and stickers) */}
|
||||
{(element.type === 'image' || element.type === 'sticker') && (
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: 'var(--text-secondary)',
|
||||
marginBottom: '0.5rem',
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
Size
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ fontSize: '10px', color: 'var(--text-muted)' }}>W</label>
|
||||
<input
|
||||
type="number"
|
||||
value={Math.round(element.width)}
|
||||
onChange={(e) => handleSizeChange('width', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.5rem',
|
||||
border: `1px solid var(--border)`,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ fontSize: '10px', color: 'var(--text-muted)' }}>H</label>
|
||||
<input
|
||||
type="number"
|
||||
value={Math.round(element.height)}
|
||||
onChange={(e) => handleSizeChange('height', e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.5rem',
|
||||
border: `1px solid var(--border)`,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Font size (for text) */}
|
||||
{element.type === 'text' && (
|
||||
<>
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: 'var(--text-secondary)',
|
||||
marginBottom: '0.5rem',
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
Font Size: {Math.round(element.fontSize)}px
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="12"
|
||||
max="120"
|
||||
value={element.fontSize}
|
||||
onChange={(e) => onUpdate({ fontSize: parseInt(e.target.value, 10) })}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: 'var(--text-secondary)',
|
||||
marginBottom: '0.5rem',
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
Color
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
value={element.fill}
|
||||
onChange={(e) => onUpdate({ fill: e.target.value })}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '36px',
|
||||
border: `1px solid var(--border)`,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
cursor: 'pointer',
|
||||
padding: '2px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Rotation */}
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: 'var(--text-secondary)',
|
||||
marginBottom: '0.5rem',
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
Rotation: {Math.round(element.rotation)}°
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="-180"
|
||||
max="180"
|
||||
value={element.rotation}
|
||||
onChange={(e) => handleRotationChange(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Delete button */}
|
||||
<button
|
||||
onClick={() => onDelete(element.id)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.75rem',
|
||||
border: 'none',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
background: 'var(--error)',
|
||||
color: '#fff',
|
||||
fontSize: '13px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = '#dc2626';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = 'var(--error)';
|
||||
}}
|
||||
>
|
||||
Delete Element
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user