feat: navigation restructure + drag-and-drop menu ordering
Navigation: - Remove Plugins header from sidebar - Merge static items and plugin items into unified sorted list - Group related menu items (Dateien, E-Mail, Kalender, Automation) - German labels for all menu items - Sort by order field, alphabetical fallback for ties Drag-and-Drop Menu: - New backend endpoints: GET/PUT /api/v1/users/me/menu-order - Store menu order in user preferences JSONB field - New SettingsMenuOrder page with @dnd-kit drag-and-drop - New Settings tab: Menu ordering - Sidebar reads saved menu order and sorts accordingly - Reset to default option
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { GripVertical, FolderOpen } from 'lucide-react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface SortableMenuItemProps {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
isGroup?: boolean;
|
||||
}
|
||||
|
||||
function getIcon(name: string): React.ReactNode {
|
||||
const Icon = (LucideIcons as any)[name];
|
||||
return Icon ? <Icon className="h-4 w-4" /> : <LucideIcons.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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user