From 5c7693902de0e9ac852b2e5102a2b66c3fdd28de Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 25 Mar 2026 10:22:35 +0100 Subject: [PATCH] feat: drag-and-drop reordering for pinned properties bar Uses @dnd-kit/sortable with horizontal strategy. Drag order persists to _pinned_properties in the type definition frontmatter. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/EditorContent.tsx | 3 + src/components/NoteItem.tsx | 2 + src/components/PinnedPropertiesBar.tsx | 94 +++++++++++++++++++------- 3 files changed, 73 insertions(+), 26 deletions(-) diff --git a/src/components/EditorContent.tsx b/src/components/EditorContent.tsx index 59ed14d7..3816445c 100644 --- a/src/components/EditorContent.tsx +++ b/src/components/EditorContent.tsx @@ -14,6 +14,9 @@ import { RawEditorView } from './RawEditorView' import { countWords } from '../utils/wikilinks' import { SingleEditorView } from './SingleEditorView' import { isEmoji } from '../utils/emoji' +import { PinnedPropertiesBar } from './PinnedPropertiesBar' +import { parseFrontmatter } from '../utils/frontmatter' +import type { FrontmatterValue } from './Inspector' import { parseFrontmatter } from '../utils/frontmatter' import { PinnedPropertiesBar } from './PinnedPropertiesBar' diff --git a/src/components/NoteItem.tsx b/src/components/NoteItem.tsx index 8a0e77b4..61d4426e 100644 --- a/src/components/NoteItem.tsx +++ b/src/components/NoteItem.tsx @@ -10,6 +10,7 @@ import { resolveIcon } from '../utils/iconRegistry' import { relativeDate, getDisplayDate } from '../utils/noteListHelpers' import { isEmoji } from '../utils/emoji' import { NoteListPinnedValues } from './NoteListPinnedValues' +import { NoteListPinnedValues } from './NoteListPinnedValues' const TYPE_ICON_MAP: Record>> = { Project: Wrench, @@ -150,6 +151,7 @@ export function NoteItem({ entry, isSelected, isMultiSelected = false, isHighlig )} {pinnedConfigs.length > 0 && } + {pinnedConfigs.length > 0 && } {entry.trashed && entry.trashedAt ? :
{relativeDate(getDisplayDate(entry))}
diff --git a/src/components/PinnedPropertiesBar.tsx b/src/components/PinnedPropertiesBar.tsx index 20b725d9..b6b622e0 100644 --- a/src/components/PinnedPropertiesBar.tsx +++ b/src/components/PinnedPropertiesBar.tsx @@ -1,4 +1,7 @@ import { memo, useRef, useState, useCallback, useEffect } from 'react' +import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core' +import { SortableContext, useSortable, horizontalListSortingStrategy } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' import type { VaultEntry } from '../types' import type { FrontmatterValue } from './Inspector' import type { ParsedFrontmatter } from '../utils/frontmatter' @@ -13,7 +16,23 @@ export const PinnedPropertiesBar = memo(function PinnedPropertiesBar({ entry, en onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue) => Promise onNavigate?: (target: string) => void }) { - const { resolved } = usePinnedProperties({ entry, entries, frontmatter, onUpdateTypeFrontmatter: onUpdateFrontmatter }) + const { resolved, pinnedConfigs, reorderPins } = usePinnedProperties({ entry, entries, frontmatter, onUpdateTypeFrontmatter: onUpdateFrontmatter }) + + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), + ) + + const handleDragEnd = useCallback((event: DragEndEvent) => { + const { active, over } = event + if (!over || active.id === over.id) return + const oldIndex = pinnedConfigs.findIndex((c) => c.key === active.id) + const newIndex = pinnedConfigs.findIndex((c) => c.key === over.id) + if (oldIndex === -1 || newIndex === -1) return + const reordered = [...pinnedConfigs] + const [moved] = reordered.splice(oldIndex, 1) + reordered.splice(newIndex, 0, moved) + reorderPins(reordered) + }, [pinnedConfigs, reorderPins]) const containerRef = useRef(null) const [visibleCount, setVisibleCount] = useState(resolved.length) @@ -49,36 +68,59 @@ export const PinnedPropertiesBar = memo(function PinnedPropertiesBar({ entry, en const hiddenCount = resolved.length - visibleCount + const sortableIds = resolved.map((p) => p.key) + return ( -
- {resolved.map((p, i) => ( -
= visibleCount ? { visibility: 'hidden', position: 'absolute' } : undefined}> - + + +
+ {resolved.map((p, i) => ( + + ))} + {hiddenCount > 0 && ( + setShowOverflow((v) => !v)} + onSave={handleSave} + onNavigate={onNavigate} + /> + )}
- ))} - {hiddenCount > 0 && ( - setShowOverflow((v) => !v)} - onSave={handleSave} - onNavigate={onNavigate} - /> - )} -
+ + ) }) +function SortableChip({ prop, visible, onSave, onNavigate }: { + prop: ResolvedPinnedProperty; visible: boolean + onSave: (key: string, value: string) => void; onNavigate?: (target: string) => void +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: prop.key }) + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + ...(visible ? {} : { visibility: 'hidden' as const, position: 'absolute' as const }), + } + + return ( +
+ +
+ ) +} + function OverflowPopover({ count, items, open, onToggle, onSave, onNavigate }: { count: number items: ResolvedPinnedProperty[]