117 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|