diff --git a/src/App.tsx b/src/App.tsx index 10265e39..24f126c3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -112,6 +112,15 @@ function App() { vault.updateEntry(typeEntry.path, { icon, color }) }, [vault, notes]) + const handleReorderSections = useCallback((orderedTypes: { typeName: string; order: number }[]) => { + for (const { typeName, order } of orderedTypes) { + const typeEntry = vault.entries.find((e) => e.isA === 'Type' && e.title === typeName) + if (!typeEntry) continue + notes.handleUpdateFrontmatter(typeEntry.path, 'order', order) + vault.updateEntry(typeEntry.path, { order }) + } + }, [vault, notes]) + useAppKeyboard({ onQuickOpen: () => setShowQuickOpen(true), onCreateNote: openCreateDialog, @@ -161,7 +170,7 @@ function App() {
- setShowCommitDialog(true)} /> + setShowCommitDialog(true)} />
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 7bc27cda..dd273aa9 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,10 +1,26 @@ import { useState, useMemo, useRef, useEffect, useCallback, memo, type ComponentType } from 'react' import type { VaultEntry, SidebarSelection } from '../types' import { cn } from '@/lib/utils' -import { ChevronRight, ChevronDown, GitCommitHorizontal, Plus, SlidersHorizontal } from 'lucide-react' +import { ChevronRight, ChevronDown, GitCommitHorizontal, Plus, SlidersHorizontal, GripVertical } from 'lucide-react' import { getTypeColor, getTypeLightColor } from '../utils/typeColors' import { resolveIcon, TypeCustomizePopover } from './TypeCustomizePopover' import { useSectionVisibility } from '../hooks/useSectionVisibility' +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + type DragEndEvent, +} from '@dnd-kit/core' +import { + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' import { FileText, Star, @@ -30,6 +46,7 @@ interface SidebarProps { onCreateType?: (type: string) => void onCreateNewType?: () => void onCustomizeType?: (typeName: string, icon: string, color: string) => void + onReorderSections?: (orderedTypes: { typeName: string; order: number }[]) => void modifiedCount?: number onCommitPush?: () => void } @@ -59,7 +76,7 @@ const BUILT_IN_SECTION_GROUPS: SectionGroup[] = [ const BUILT_IN_TYPES = new Set(BUILT_IN_SECTION_GROUPS.map((s) => s.type)) -export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onSelectNote, onCreateType, onCreateNewType, onCustomizeType, modifiedCount = 0, onCommitPush }: SidebarProps) { +export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onSelectNote, onCreateType, onCreateNewType, onCustomizeType, onReorderSections, modifiedCount = 0, onCommitPush }: SidebarProps) { const [collapsed, setCollapsed] = useState>({}) const [customizeTarget, setCustomizeTarget] = useState(null) const [contextMenuPos, setContextMenuPos] = useState<{ x: number; y: number } | null>(null) @@ -130,11 +147,47 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS }) }, [typeEntryMap]) - const allSectionGroups = useMemo( - () => [...builtInWithOverrides, ...customSectionGroups], - [builtInWithOverrides, customSectionGroups], + // Merge built-in and custom sections, then sort by order from type entries + const allSectionGroups = useMemo(() => { + const merged = [...builtInWithOverrides, ...customSectionGroups] + return merged.sort((a, b) => { + const orderA = typeEntryMap[a.type]?.order ?? Infinity + const orderB = typeEntryMap[b.type]?.order ?? Infinity + if (orderA !== orderB) return orderA - orderB + return a.label.localeCompare(b.label) + }) + }, [builtInWithOverrides, customSectionGroups, typeEntryMap]) + + // DnD sensors with activation distance to avoid accidental drags on click + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ) + const visibleSections = useMemo( + () => allSectionGroups.filter((g) => isSectionVisible(g.type)), + [allSectionGroups, isSectionVisible], + ) + + const sectionIds = useMemo(() => visibleSections.map((g) => g.type), [visibleSections]) + + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event + if (!over || active.id === over.id) return + + const oldIndex = sectionIds.indexOf(active.id as string) + const newIndex = sectionIds.indexOf(over.id as string) + if (oldIndex === -1 || newIndex === -1) return + + // Compute new order: assign sequential order values to the reordered list + const reordered = [...sectionIds] + reordered.splice(oldIndex, 1) + reordered.splice(newIndex, 0, active.id as string) + + const updates = reordered.map((typeName, i) => ({ typeName, order: i })) + onReorderSections?.(updates) + }, [sectionIds, onReorderSections]) + const archivedCount = useMemo(() => entries.filter((e) => e.archived).length, [entries]) // Close context menu on outside click @@ -191,7 +244,7 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS } }, [customizeTarget, typeEntryMap, onCustomizeType]) - const renderSection = ({ label, type, Icon, customColor }: SectionGroup) => { + const renderSectionContent = ({ label, type, Icon, customColor }: SectionGroup, dragHandleProps?: Record) => { const items = entries.filter((e) => e.isA === type && !e.archived) const isCollapsed = collapsed[type] ?? true const isTopic = type === 'Topic' @@ -209,7 +262,7 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS } return ( -
+ <> {/* Section header row */}
onSelect({ kind: 'sectionGroup', type })} onContextMenu={(e) => handleContextMenu(e, type)} > -
+
+ {/* Drag handle */} +
+ +
- {label} + {label}
{(onCreateType || (isTypeSection && onCreateNewType)) && ( @@ -280,6 +342,30 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS ))}
)} + + ) + } + + function SortableSection({ group }: { group: SectionGroup }) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: group.type }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + padding: '4px 6px', + } + + return ( +
+ {renderSectionContent(group, listeners)}
) } @@ -428,8 +514,14 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS )}
- {/* Section Groups (built-in + custom), filtered by visibility */} - {allSectionGroups.filter((g) => isSectionVisible(g.type)).map(renderSection)} + {/* Section Groups (built-in + custom), filtered by visibility, sortable by drag */} + + + {visibleSections.map((g) => ( + + ))} + + {/* Commit button — always visible */}