summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-05-08 12:17:07 +0100
committerGitHub <noreply@github.com>2026-05-08 12:17:07 +0100
commit2dda1f96a30eee2fda86be9a8a59ce5cb26dad7f (patch)
tree55535f138665f620a5e37a192b0a6fd91b859e26 /makima/frontend
parent6690b714c64aaef5781bc0aac41b777ab72e9070 (diff)
downloadsoryu-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.tsx86
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"