feat: integrate Radix UI toggle components and replace custom toast implementation with Sonner

- Added @radix-ui/react-toggle and @radix-ui/react-toggle-group to package dependencies.
- Implemented ToggleGroup and Toggle components for improved UI interaction.
- Replaced custom toast notifications with Sonner for better user feedback.
- Updated CanvasPage and ProjectsPage to utilize new toast notifications.
- Removed obsolete toast context and provider.
- Enhanced ProjectsPage with a toggle view for project display (cards/table).
This commit is contained in:
2026-03-09 23:43:05 +01:00
parent 57dfcb6458
commit 3ad02cfb76
8 changed files with 557 additions and 215 deletions

View File

@@ -57,6 +57,7 @@ import {
isConnectionAllowed,
} from '@/lib/nodeRegistry'
import type { AppNode, AppEdge } from '@/lib/nodeTypes'
import { toast } from 'sonner'
import {
loadGraphFromStorage,
saveGraphToStorage,
@@ -116,8 +117,6 @@ function getExampleGraph(): { nodes: AppNode[]; edges: AppEdge[] } {
}
}
export type CanvasMessage = { type: 'success' | 'error'; text: string }
function FlowFitViewOnLoad() {
const nodesInitialized = useNodesInitialized()
const { fitView } = useReactFlow()
@@ -178,7 +177,6 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
const [rfInstance, setRfInstance] = React.useState<unknown>(null)
const [renamingNodeId, setRenamingNodeId] = React.useState<string | null>(null)
const [connectionFrom, setConnectionFrom] = React.useState<{ nodeId: string; sourceHandle?: string } | null>(null)
const [canvasMessage, setCanvasMessage] = React.useState<CanvasMessage | null>(null)
const wrapperRef = useRef<HTMLDivElement | null>(null)
const lastClickRef = useRef<{ clientX: number; clientY: number } | null>(null)
const flowActionsRef = useRef<{ pasteAtViewportCenter: () => void; fitView: () => void } | null>(null)
@@ -279,12 +277,6 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
const onInit = useCallback((instance: unknown) => setRfInstance(instance), [])
React.useEffect(() => {
if (!canvasMessage) return
const t = setTimeout(() => setCanvasMessage(null), 3000)
return () => clearTimeout(t)
}, [canvasMessage])
const onNodeDragStart = useCallback(() => saveForDragEnd(), [saveForDragEnd])
const onNodeDragStop = useCallback(() => commitDragEnd(), [commitDragEnd])
@@ -297,7 +289,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
a.download = `project${PROJECT_FILE_EXT}`
a.click()
URL.revokeObjectURL(url)
setCanvasMessage({ type: 'success', text: 'Project exported' })
toast.success('Project exported')
}, [nodes, edges])
const handleImportProject = useCallback(() => importInputRef.current?.click(), [])
@@ -305,7 +297,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
const handleLoadExample = useCallback(() => {
const { nodes: exampleNodes, edges: exampleEdges } = getExampleGraph()
setStateImmediate({ nodes: exampleNodes, edges: exampleEdges })
setCanvasMessage({ type: 'success', text: 'Example loaded' })
toast.success('Example loaded')
}, [setStateImmediate])
const onImportFileChange = useCallback(
@@ -319,17 +311,17 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
const text = reader.result as string
const state = JSON.parse(text) as { version?: number; nodes?: unknown[]; edges?: unknown[] }
if (!state || !Array.isArray(state.nodes) || !Array.isArray(state.edges)) {
setCanvasMessage({ type: 'error', text: 'Invalid file: expected nodes and edges arrays' })
toast.error('Invalid file: expected nodes and edges arrays')
return
}
setStateImmediate({ nodes: state.nodes as AppNode[], edges: state.edges as AppEdge[] })
setCanvasMessage(
state.version != null && state.version > PROJECT_VERSION
? { type: 'error', text: 'Project was created with a newer app version' }
: { type: 'success', text: 'Project loaded' }
)
if (state.version != null && state.version > PROJECT_VERSION) {
toast.error('Project was created with a newer app version')
} else {
toast.success('Project loaded')
}
} catch {
setCanvasMessage({ type: 'error', text: 'Invalid file: not valid JSON' })
toast.error('Invalid file: not valid JSON')
}
}
reader.readAsText(file)
@@ -517,17 +509,6 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
Backend API: {apiTodosCount >= 0 ? `${apiTodosCount} todos` : 'unavailable'}
</div>
)}
{canvasMessage && (
<div
role="status"
className={`shrink-0 px-3 py-2 text-sm ${canvasMessage.type === 'success'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200'
: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-200'
}`}
>
{canvasMessage.text}
</div>
)}
<div className="flex-1 min-h-0 relative flex flex-col">
<div className="flex-1 min-h-0 flex flex-col">
<FlowContext.Provider value={flowContextValue}>