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 */}