ChunDe's picture
feat: Polish LayerOrder selector with animations, keyboard shortcuts, and enhanced UX
9d74698
raw
history blame
6.09 kB
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('');
// Get icon based on object type
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" />;
}
};
// Get label for object
const getObjectLabel = () => {
// Use custom name if available
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;
}
// Capitalize first letter of type
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>
</>
);
}