Phase 7.2 - Debounce undo/redo history: - Add 300ms debounce timer for rapid drag/transform changes - Commit history on dragEnd/transformEnd events only - Prevents history bloat during continuous interactions Phase 8.3 - Template-aware export: - Render template background layer first - Apply slot crop regions for image elements - Render template overlay layer last - Support nonPrintable flag for guides/watermarks Phase 9 - PWA icons: - Add pwa-192x192.svg and pwa-512x512.svg icons - Update vite.config.js manifest configuration Phase 10.3 - Performance optimizations: - Add React.memo to canvas components (ImageElement, TextElement, DesignCanvas) - Add React.memo to panel components (LayersPanel, PropertiesPanel) - Prevent unnecessary re-renders during canvas interactions Phase 10.6 - Template documentation: - Document template JSON schema in docs/template-schema.md - Include element properties, slot definitions, and examples - Describe background/overlay layer structure
134 lines
3.9 KiB
JavaScript
134 lines
3.9 KiB
JavaScript
import { memo } from 'react';
|
||
|
||
export const LayersPanel = memo(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>
|
||
);
|
||
});
|