15 changed files with 822 additions and 152 deletions
@ -0,0 +1,101 @@ |
|||
"use client"; |
|||
|
|||
import { useWorkflowStore } from "@/store/workflowStore"; |
|||
import { useMemo, useEffect, useState, useRef } from "react"; |
|||
|
|||
export function EdgeToolbar() { |
|||
const { edges, toggleEdgePause, removeEdge } = useWorkflowStore(); |
|||
const [clickPosition, setClickPosition] = useState<{ x: number; y: number } | null>(null); |
|||
const previousSelectedEdgeId = useRef<string | null>(null); |
|||
|
|||
const selectedEdge = useMemo( |
|||
() => edges.find((edge) => edge.selected), |
|||
[edges] |
|||
); |
|||
|
|||
// Track mouse position when edge selection changes
|
|||
useEffect(() => { |
|||
const handleMouseDown = (e: MouseEvent) => { |
|||
// Check if clicking on an edge
|
|||
const target = e.target as Element; |
|||
if (target.closest('.react-flow__edge')) { |
|||
setClickPosition({ x: e.clientX, y: e.clientY - 40 }); // 40px above click
|
|||
} |
|||
}; |
|||
|
|||
document.addEventListener('mousedown', handleMouseDown); |
|||
return () => document.removeEventListener('mousedown', handleMouseDown); |
|||
}, []); |
|||
|
|||
// Reset click position when edge is deselected
|
|||
useEffect(() => { |
|||
if (!selectedEdge && previousSelectedEdgeId.current) { |
|||
setClickPosition(null); |
|||
} |
|||
previousSelectedEdgeId.current = selectedEdge?.id || null; |
|||
}, [selectedEdge]); |
|||
|
|||
const toolbarPosition = clickPosition; |
|||
|
|||
const handleTogglePause = () => { |
|||
if (selectedEdge) { |
|||
toggleEdgePause(selectedEdge.id); |
|||
} |
|||
}; |
|||
|
|||
const handleDelete = () => { |
|||
if (selectedEdge) { |
|||
removeEdge(selectedEdge.id); |
|||
} |
|||
}; |
|||
|
|||
if (!toolbarPosition || !selectedEdge) return null; |
|||
|
|||
const hasPause = selectedEdge.data?.hasPause; |
|||
|
|||
return ( |
|||
<div |
|||
className="fixed z-[100] flex items-center gap-1 bg-neutral-800 border border-neutral-600 rounded-lg shadow-xl p-1" |
|||
style={{ |
|||
left: toolbarPosition.x, |
|||
top: toolbarPosition.y, |
|||
transform: "translateX(-50%)", |
|||
}} |
|||
> |
|||
<button |
|||
onClick={handleTogglePause} |
|||
className={`p-1.5 rounded hover:bg-neutral-700 transition-colors ${ |
|||
hasPause |
|||
? "text-amber-400 hover:text-amber-300" |
|||
: "text-neutral-400 hover:text-neutral-100" |
|||
}`}
|
|||
title={hasPause ? "Remove pause" : "Add pause"} |
|||
> |
|||
{hasPause ? ( |
|||
// Play icon (resume)
|
|||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"> |
|||
<path d="M8 5v14l11-7z" /> |
|||
</svg> |
|||
) : ( |
|||
// Pause icon
|
|||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"> |
|||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /> |
|||
</svg> |
|||
)} |
|||
</button> |
|||
<button |
|||
onClick={handleDelete} |
|||
className="p-1.5 rounded hover:bg-neutral-700 text-neutral-400 hover:text-red-400 transition-colors" |
|||
title="Delete" |
|||
> |
|||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> |
|||
<path |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" |
|||
/> |
|||
</svg> |
|||
</button> |
|||
</div> |
|||
); |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
"use client"; |
|||
|
|||
import { useEffect } from "react"; |
|||
import { create } from "zustand"; |
|||
|
|||
interface ToastState { |
|||
message: string | null; |
|||
type: "info" | "success" | "warning" | "error"; |
|||
show: (message: string, type?: "info" | "success" | "warning" | "error") => void; |
|||
hide: () => void; |
|||
} |
|||
|
|||
export const useToast = create<ToastState>((set) => ({ |
|||
message: null, |
|||
type: "info", |
|||
show: (message, type = "info") => set({ message, type }), |
|||
hide: () => set({ message: null }), |
|||
})); |
|||
|
|||
const typeStyles = { |
|||
info: "bg-neutral-800 border-neutral-600 text-neutral-100", |
|||
success: "bg-green-900 border-green-700 text-green-100", |
|||
warning: "bg-orange-900 border-orange-600 text-orange-100", |
|||
error: "bg-red-900 border-red-700 text-red-100", |
|||
}; |
|||
|
|||
const typeIcons = { |
|||
info: ( |
|||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
|||
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|||
</svg> |
|||
), |
|||
success: ( |
|||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
|||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|||
</svg> |
|||
), |
|||
warning: ( |
|||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> |
|||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /> |
|||
</svg> |
|||
), |
|||
error: ( |
|||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
|||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|||
</svg> |
|||
), |
|||
}; |
|||
|
|||
export function Toast() { |
|||
const { message, type, hide } = useToast(); |
|||
|
|||
useEffect(() => { |
|||
if (message) { |
|||
const timer = setTimeout(() => { |
|||
hide(); |
|||
}, 4000); |
|||
return () => clearTimeout(timer); |
|||
} |
|||
}, [message, hide]); |
|||
|
|||
if (!message) return null; |
|||
|
|||
return ( |
|||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[200] animate-in fade-in slide-in-from-bottom-4 duration-300"> |
|||
<div |
|||
className={`flex items-center gap-3 px-4 py-3 rounded-lg border shadow-xl ${typeStyles[type]}`} |
|||
> |
|||
{typeIcons[type]} |
|||
<span className="text-sm font-medium">{message}</span> |
|||
<button |
|||
onClick={hide} |
|||
className="ml-2 p-1 rounded hover:bg-white/10 transition-colors" |
|||
> |
|||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> |
|||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> |
|||
</svg> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
Loading…
Reference in new issue