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,131 @@
export function LayersPanel({ elements, selectedId, onSelect, onDelete }) {
const getIcon = (element) => {
switch (element.type) {
case 'image':
return element.bgRemoved ? '🖼️' : '📷';
case 'text':
return '📝';
case 'sticker':
return '🎨';
default:
return '📁';
}
};
const getName = (element) => {
switch (element.type) {
case 'image':
return element.bgRemoved ? 'Image (BG ✓)' : 'Image';
case 'text':
return element.text?.substring(0, 20) || 'Text';
case 'sticker':
return 'Sticker';
default:
return 'Element';
}
};
if (elements.length === 0) {
return (
<div style={{
padding: '1rem',
textAlign: 'center',
color: 'var(--text-muted)',
fontSize: '12px',
}}>
No elements yet. Add images, text, or stickers to your design.
</div>
);
}
return (
<div>
<h3 style={{
margin: '0 0 0.75rem 0',
fontSize: '12px',
fontWeight: '600',
color: 'var(--text-secondary)',
textTransform: 'uppercase',
}}>
Layers ({elements.length})
</h3>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '4px',
}}>
{elements.map((element, index) => (
<div
key={element.id}
onClick={() => onSelect(element.id)}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 0.75rem',
background: selectedId === element.id ? 'var(--accent-bg)' : 'transparent',
border: `1px solid ${selectedId === element.id ? 'var(--accent)' : 'var(--border)'}`,
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
transition: 'all 0.15s ease',
}}
onMouseEnter={(e) => {
if (selectedId !== element.id) {
e.target.style.borderColor = 'var(--accent)';
}
}}
onMouseLeave={(e) => {
if (selectedId !== element.id) {
e.target.style.borderColor = 'var(--border)';
}
}}
>
<span style={{ fontSize: '14px' }}>{getIcon(element)}</span>
<span style={{
flex: 1,
fontSize: '12px',
color: selectedId === element.id ? 'var(--accent)' : 'var(--text-primary)',
fontWeight: selectedId === element.id ? '600' : '400',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>
{getName(element)}
</span>
<button
onClick={(e) => {
e.stopPropagation();
onDelete(element.id);
}}
style={{
width: '24px',
height: '24px',
border: 'none',
borderRadius: 'var(--radius-sm)',
background: 'transparent',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '14px',
color: 'var(--text-muted)',
transition: 'all 0.15s ease',
}}
onMouseEnter={(e) => {
e.target.style.background = 'var(--error)';
e.target.style.color = '#fff';
}}
onMouseLeave={(e) => {
e.target.style.background = 'transparent';
e.target.style.color = 'var(--text-muted)';
}}
>
×
</button>
</div>
))}
</div>
</div>
);
}