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:
131
client/src/components/panels/LayersPanel.jsx
Normal file
131
client/src/components/panels/LayersPanel.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user