Files
leocrm/frontend/src/components/layout/SortableMenuItem.tsx
T

117 lines
2.6 KiB
TypeScript

import React from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import {
Activity,
ArrowUpDown,
BarChart3,
Bell,
BookOpen,
Bot,
Calendar,
CheckSquare,
Copy,
FileText,
FolderOpen,
Link as LinkIcon,
Mail,
MessageSquare,
Settings,
Shield,
Sparkles,
Tag,
GripVertical,
Trash2,
Users,
UsersRound,
Workflow,
} from 'lucide-react';
import clsx from 'clsx';
// Curated icon map — avoids `import * as LucideIcons` which loads ALL icons
// and causes OOM in tests (ARCH-063).
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
Activity,
ArrowUpDown,
BarChart: BarChart3,
BarChart3,
Bell,
BookOpen,
Bot,
Calendar,
CheckSquare,
Copy,
FolderOpen,
Link: LinkIcon,
Mail,
MessageSquare,
Settings,
Shield,
Sparkles,
Tag,
Trash: Trash2,
Trash2,
Users,
UsersRound,
Workflow,
};
interface SortableMenuItemProps {
id: string;
label: string;
icon: string;
isGroup?: boolean;
}
function getIcon(name: string): React.ReactNode {
const Icon = ICON_MAP[name];
return Icon ? <Icon className="h-4 w-4" /> : <FileText className="h-4 w-4" />;
}
export function SortableMenuItem({ id, label, icon, isGroup }: SortableMenuItemProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
className={clsx(
'flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium',
'bg-white border border-secondary-200',
'motion-safe:transition-shadow',
isDragging && 'shadow-lg ring-2 ring-primary-500 z-50',
!isDragging && 'hover:bg-secondary-50'
)}
>
<button
className="cursor-grab active:cursor-grabbing text-secondary-400 hover:text-secondary-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 rounded"
{...attributes}
{...listeners}
aria-label="Ziehen zum Sortieren"
type="button"
>
<GripVertical className="w-4 h-4" />
</button>
<span className="text-secondary-400">
{isGroup ? <FolderOpen className="h-4 w-4" /> : getIcon(icon)}
</span>
<span className="flex-1 truncate">
{isGroup && <span className="text-xs font-semibold uppercase tracking-wider text-secondary-500 mr-2">{label}</span>}
{!isGroup && label}
</span>
</div>
);
}