From 2dda1f96a30eee2fda86be9a8a59ce5cb26dad7f Mon Sep 17 00:00:00 2001 From: soryu Date: Fri, 8 May 2026 12:17:07 +0100 Subject: feat(contracts): drag-to-reorder active contract rows in sidebar (#130) HTML5 drag/drop on active contract rows. Dragging a row over another in the same directive folder shows a green top-border drop indicator; dropping calls reorderDirectiveContract(id, targetPosition) and refreshes the folder. Shipped/archived rows aren't draggable (historical, ordering is fixed). Implementation: - DocumentRow gains optional draggable + drag event props. - DirectiveFolder owns the drag/over state and handleReorder callback; computes target position from the drop-target row's current position. - Repository's reorder endpoint already exists from the backbone PR and handles sibling shift in a transaction. Co-authored-by: Claude Opus 4.7 (1M context) --- makima/frontend/src/routes/document-directives.tsx | 86 +++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) (limited to 'makima/frontend/src/routes') diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx index 044c8af..b89e841 100644 --- a/makima/frontend/src/routes/document-directives.tsx +++ b/makima/frontend/src/routes/document-directives.tsx @@ -22,6 +22,7 @@ import { pauseDirectiveContract, completeDirectiveContract, unlockDirectiveContract, + reorderDirectiveContract, createDirectiveTask, startDirective, pauseDirective, @@ -283,6 +284,30 @@ function DirectiveFolder({ ? selection.documentId : null; + // Drag-to-reorder state — only active contracts (draft/queued/active) are + // reorderable. The drag id lives in the folder so the drop target can read + // it from props. `dragOverId` powers the visual indicator on hover. + const [dragId, setDragId] = useState(null); + const [dragOverId, setDragOverId] = useState(null); + + const handleReorder = useCallback( + async (draggedId: string, targetDoc: Contract) => { + setDragId(null); + setDragOverId(null); + // No-op if dropping on self. + if (draggedId === targetDoc.id) return; + try { + await reorderDirectiveContract(draggedId, targetDoc.position); + await refresh(); + } catch (e) { + // Fail open — sidebar will refresh on next refreshNonce bump. + // eslint-disable-next-line no-console + console.error("Reorder failed", e); + } + }, + [refresh], + ); + return (
{/* Directive folder header. Status is shown as a colored dot on the @@ -383,6 +408,22 @@ function DirectiveFolder({ directive={directive} selected={doc.id === selectedDocumentId} onSelect={() => onSelectDocument(directive.id, doc)} + draggable + onDragStart={() => setDragId(doc.id)} + onDragEnd={() => { + setDragId(null); + setDragOverId(null); + }} + onDragOver={() => { + if (dragId && dragId !== doc.id) setDragOverId(doc.id); + }} + onDragLeave={() => { + if (dragOverId === doc.id) setDragOverId(null); + }} + onDrop={() => { + if (dragId) void handleReorder(dragId, doc); + }} + dragOver={dragOverId === doc.id} /> void; indent?: "normal" | "deep"; + // ----- Drag-to-reorder props (optional — only wired by the active list) --- + /** Whether this row participates in HTML5 drag (active docs only). */ + draggable?: boolean; + onDragStart?: () => void; + onDragEnd?: () => void; + onDragOver?: () => void; + onDragLeave?: () => void; + onDrop?: () => void; + /** True while a drag is hovering over this row — drives the drop indicator. */ + dragOver?: boolean; } function DocumentRow({ @@ -468,17 +519,50 @@ function DocumentRow({ selected, onSelect, indent = "normal", + draggable = false, + onDragStart, + onDragEnd, + onDragOver, + onDragLeave, + onDrop, + dragOver = false, }: DocumentRowProps) { const dot = DOC_STATUS_DOT[doc.status] ?? DOC_STATUS_DOT.draft; const padLeft = indent === "deep" ? "pl-[88px]" : "pl-14"; const name = `${fileLabel(doc, directive)}.md`; + // Drop-indicator: a top border accent on the hovered target row. + const dropAccent = dragOver + ? "border-t-2 border-t-emerald-400" + : "border-t-2 border-t-transparent"; + return (