diff options
| author | soryu <soryu@soryu.co> | 2026-05-08 12:17:07 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-08 12:17:07 +0100 |
| commit | 2dda1f96a30eee2fda86be9a8a59ce5cb26dad7f (patch) | |
| tree | 55535f138665f620a5e37a192b0a6fd91b859e26 /makima/frontend | |
| parent | 6690b714c64aaef5781bc0aac41b777ab72e9070 (diff) | |
| download | soryu-2dda1f96a30eee2fda86be9a8a59ce5cb26dad7f.tar.gz soryu-2dda1f96a30eee2fda86be9a8a59ce5cb26dad7f.zip | |
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) <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend')
| -rw-r--r-- | makima/frontend/src/routes/document-directives.tsx | 86 |
1 files changed, 85 insertions, 1 deletions
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<string | null>(null); + const [dragOverId, setDragOverId] = useState<string | null>(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 ( <div className="select-none"> {/* 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} /> <DocumentTasksFolder documentId={doc.id} @@ -460,6 +501,16 @@ interface DocumentRowProps { selected: boolean; onSelect: () => 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 ( <button type="button" onClick={onSelect} title={name} - className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${ + draggable={draggable} + onDragStart={(e) => { + if (!draggable) return; + // Required by Firefox to actually start the drag. + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", doc.id); + onDragStart?.(); + }} + onDragEnd={onDragEnd} + onDragOver={(e) => { + if (!draggable) return; + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + onDragOver?.(); + }} + onDragLeave={onDragLeave} + onDrop={(e) => { + if (!draggable) return; + e.preventDefault(); + onDrop?.(); + }} + className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${dropAccent} ${ selected ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]" : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent" |
