|
|
import { useState } from 'react'; |
|
|
import { CanvasObject } from '../../types/canvas.types'; |
|
|
import { Type, Image as ImageIcon, Square, HelpCircle } from 'lucide-react'; |
|
|
|
|
|
interface LayerItemProps { |
|
|
object: CanvasObject; |
|
|
index: number; |
|
|
isSelected: boolean; |
|
|
isDragging: boolean; |
|
|
isDropTarget: boolean; |
|
|
onSelect: (id: string, shiftKey: boolean) => void; |
|
|
onDragStart: (id: string) => void; |
|
|
onDragOver: (index: number) => void; |
|
|
onDragEnd: (draggedId: string, targetIndex: number) => void; |
|
|
onNameChange?: (id: string, name: string) => void; |
|
|
} |
|
|
|
|
|
export default function LayerItem({ |
|
|
object, |
|
|
index, |
|
|
isSelected, |
|
|
isDragging, |
|
|
isDropTarget, |
|
|
onSelect, |
|
|
onDragStart, |
|
|
onDragOver, |
|
|
onDragEnd, |
|
|
onNameChange |
|
|
}: LayerItemProps) { |
|
|
const [isHovered, setIsHovered] = useState(false); |
|
|
const [isEditing, setIsEditing] = useState(false); |
|
|
const [editName, setEditName] = useState(''); |
|
|
|
|
|
|
|
|
const getObjectIcon = () => { |
|
|
switch (object.type) { |
|
|
case 'text': |
|
|
return <Type size={16} color="#ffffff" />; |
|
|
case 'image': |
|
|
case 'huggy': |
|
|
return <ImageIcon size={16} color="#ffffff" />; |
|
|
case 'rect': |
|
|
return <Square size={16} color={object.fill || '#ffffff'} />; |
|
|
case 'logoPlaceholder': |
|
|
return <HelpCircle size={16} color="#ffffff" />; |
|
|
default: |
|
|
return <Square size={16} color="#ffffff" />; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const getObjectLabel = () => { |
|
|
|
|
|
if (object.name) { |
|
|
return object.name.length > 8 ? object.name.substring(0, 8) + '...' : object.name; |
|
|
} |
|
|
|
|
|
if (object.type === 'text') { |
|
|
const text = object.text || 'Text'; |
|
|
return text.length > 8 ? text.substring(0, 8) + '...' : text; |
|
|
} |
|
|
|
|
|
|
|
|
return object.type.charAt(0).toUpperCase() + object.type.slice(1); |
|
|
}; |
|
|
|
|
|
const getFullLabel = () => { |
|
|
if (object.name) return object.name; |
|
|
if (object.type === 'text') return object.text || 'Text'; |
|
|
return object.type.charAt(0).toUpperCase() + object.type.slice(1); |
|
|
}; |
|
|
|
|
|
const getTooltip = () => { |
|
|
const name = getFullLabel(); |
|
|
const width = Math.round(object.width); |
|
|
const height = Math.round(object.height); |
|
|
const zIndex = object.zIndex; |
|
|
return `${name}\nLayer: ${zIndex}\nSize: ${width} × ${height}px`; |
|
|
}; |
|
|
|
|
|
const handleDragStart = (e: React.DragEvent) => { |
|
|
e.dataTransfer.effectAllowed = 'move'; |
|
|
e.dataTransfer.setData('text/plain', object.id); |
|
|
onDragStart(object.id); |
|
|
}; |
|
|
|
|
|
const handleDragOver = (e: React.DragEvent) => { |
|
|
e.preventDefault(); |
|
|
e.dataTransfer.dropEffect = 'move'; |
|
|
onDragOver(index); |
|
|
}; |
|
|
|
|
|
const handleDrop = (e: React.DragEvent) => { |
|
|
e.preventDefault(); |
|
|
const draggedId = e.dataTransfer.getData('text/plain'); |
|
|
onDragEnd(draggedId, index); |
|
|
}; |
|
|
|
|
|
const handleClick = (e: React.MouseEvent) => { |
|
|
onSelect(object.id, e.shiftKey); |
|
|
}; |
|
|
|
|
|
const handleDoubleClick = (e: React.MouseEvent) => { |
|
|
e.stopPropagation(); |
|
|
if (onNameChange) { |
|
|
setEditName(getFullLabel()); |
|
|
setIsEditing(true); |
|
|
} |
|
|
}; |
|
|
|
|
|
const handleNameBlur = () => { |
|
|
if (onNameChange && editName.trim() !== '') { |
|
|
onNameChange(object.id, editName.trim()); |
|
|
} |
|
|
setIsEditing(false); |
|
|
}; |
|
|
|
|
|
const handleNameKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { |
|
|
if (e.key === 'Enter') { |
|
|
handleNameBlur(); |
|
|
} else if (e.key === 'Escape') { |
|
|
setIsEditing(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<> |
|
|
{/* Drop indicator line */} |
|
|
{isDropTarget && ( |
|
|
<div |
|
|
style={{ |
|
|
height: '3px', |
|
|
background: '#3faee6', |
|
|
marginBottom: '4px', |
|
|
borderRadius: '2px', |
|
|
boxShadow: '0 0 8px rgba(63, 174, 230, 0.6)', |
|
|
animation: 'layerPulse 1s ease-in-out infinite' |
|
|
}} |
|
|
/> |
|
|
)} |
|
|
|
|
|
<div |
|
|
draggable={!isEditing} |
|
|
onDragStart={handleDragStart} |
|
|
onDragOver={handleDragOver} |
|
|
onDrop={handleDrop} |
|
|
onClick={handleClick} |
|
|
onDoubleClick={handleDoubleClick} |
|
|
onMouseEnter={() => setIsHovered(true)} |
|
|
onMouseLeave={() => setIsHovered(false)} |
|
|
style={{ |
|
|
display: 'flex', |
|
|
alignItems: 'center', |
|
|
gap: '6px', |
|
|
height: '36px', |
|
|
padding: '4px 8px', |
|
|
marginBottom: '2px', |
|
|
background: isSelected ? '#3faee6' : (isHovered ? 'rgba(255, 255, 255, 0.1)' : 'transparent'), |
|
|
borderRadius: '4px', |
|
|
cursor: isDragging ? 'grabbing' : (isEditing ? 'text' : 'grab'), |
|
|
opacity: isDragging ? 0.5 : 1, |
|
|
transition: 'background 0.15s ease, opacity 0.15s ease', |
|
|
userSelect: 'none' |
|
|
}} |
|
|
title={getTooltip()} |
|
|
> |
|
|
{/* Object icon */} |
|
|
<div style={{ flexShrink: 0 }}> |
|
|
{getObjectIcon()} |
|
|
</div> |
|
|
|
|
|
{/* Object label or input */} |
|
|
{isEditing ? ( |
|
|
<input |
|
|
type="text" |
|
|
value={editName} |
|
|
onChange={(e) => setEditName(e.target.value)} |
|
|
onBlur={handleNameBlur} |
|
|
onKeyDown={handleNameKeyDown} |
|
|
autoFocus |
|
|
style={{ |
|
|
flex: 1, |
|
|
background: 'rgba(255, 255, 255, 0.2)', |
|
|
border: '1px solid #3faee6', |
|
|
borderRadius: '2px', |
|
|
color: '#ffffff', |
|
|
fontSize: '11px', |
|
|
fontFamily: 'Inter, sans-serif', |
|
|
padding: '2px 4px', |
|
|
outline: 'none' |
|
|
}} |
|
|
onClick={(e) => e.stopPropagation()} |
|
|
/> |
|
|
) : ( |
|
|
<span |
|
|
style={{ |
|
|
color: '#ffffff', |
|
|
fontSize: '11px', |
|
|
fontWeight: isSelected ? '600' : 'normal', |
|
|
fontFamily: 'Inter, sans-serif', |
|
|
whiteSpace: 'nowrap', |
|
|
overflow: 'hidden', |
|
|
textOverflow: 'ellipsis', |
|
|
flex: 1 |
|
|
}} |
|
|
> |
|
|
{getObjectLabel()} |
|
|
</span> |
|
|
)} |
|
|
</div> |
|
|
</> |
|
|
); |
|
|
} |
|
|
|