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:
Khalid A
2026-04-21 01:27:59 -05:00
parent e67017b259
commit fd11a36d93
13 changed files with 1375 additions and 36 deletions

View 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>
);
}