From 0cb0eea0622fe7192cc0cc2d3d8bd55c322bf66a Mon Sep 17 00:00:00 2001 From: Nate Ferrell Date: Mon, 30 Sep 2024 23:06:25 -0500 Subject: [PATCH] feat: #comment moved to reactflow --- package-lock.json | 228 +++++++++++++++++++++++ package.json | 1 + src/App.tsx | 292 ++++++++++++------------------ src/components/ConnectionLine.tsx | 48 ----- src/components/CustomNode.tsx | 65 +++++++ src/components/DeviceNode.tsx | 205 --------------------- src/hooks/useAudioDeviceStore.ts | 1 - src/reactflow-dark.css | 41 +++++ 8 files changed, 452 insertions(+), 429 deletions(-) delete mode 100644 src/components/ConnectionLine.tsx create mode 100644 src/components/CustomNode.tsx delete mode 100644 src/components/DeviceNode.tsx create mode 100644 src/reactflow-dark.css diff --git a/package-lock.json b/package-lock.json index 8c954c5..4660210 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-query": "^5.56.2", "@tanstack/react-query-devtools": "^5.58.0", + "@xyflow/react": "^12.3.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -2196,6 +2197,55 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", + "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", + "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2500,6 +2550,36 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/@xyflow/react": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.3.1.tgz", + "integrity": "sha512-PurYFxwzJa0U6RRX9k4VbNRU+vQd6mRKFR8Uk1dF81diCKZDj495y6AupqsjMHtkO66tGHV0LdenLpIHvnOEFw==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.43", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.43.tgz", + "integrity": "sha512-1zHgad1cWr1mKm2xbFaarK0Jg8WRgaQ8ubSBIo/pRdq3fEgCuqgNkL9NSAP6Rvm8zi3+Lu4JPUMN+EEx5QgX9A==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -2819,6 +2899,12 @@ "node": ">= 6" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2895,6 +2981,111 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -5157,6 +5348,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5419,6 +5619,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz", + "integrity": "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index f8bdbe9..c965ab1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-query": "^5.56.2", "@tanstack/react-query-devtools": "^5.58.0", + "@xyflow/react": "^12.3.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/src/App.tsx b/src/App.tsx index 3f5b3ef..9789374 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,36 @@ // src/App.tsx -import { useState, useEffect, useRef } from 'react' +import React, { useState, useCallback, useEffect, useMemo } from 'react' +import { + ReactFlow, + MiniMap, + Controls, + Background, + useNodesState, + useEdgesState, + addEdge, + Connection, + Edge, +} from '@xyflow/react' +import '@xyflow/react/dist/style.css' +import './reactflow-dark.css' + import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import * as Dialog from '@radix-ui/react-dialog' import { useAudioDeviceStore } from './hooks/useAudioDeviceStore' -import DeviceNode from './components/DeviceNode' -import ConnectionLine from './components/ConnectionLine' +import CustomNode from './components/CustomNode' import DarkModeToggle from './components/DarkModeToggle' import AddDeviceForm from './components/AddDeviceForm' -import { Device, Connection } from './types/devices' -import { DOT_SIZE, GRID_SIZE } from './constants/grid' +import { Device } from './types/devices' import EditDeviceForm from './components/EditDeviceForm' const queryClient = new QueryClient() +const nodeTypes = { + customNode: CustomNode, +} + function AudioDeviceArrangerApp() { const { devices, @@ -23,32 +39,93 @@ function AudioDeviceArrangerApp() { updateDevicePosition, updateDevice, deleteDevice, + addConnection, } = useAudioDeviceStore() + const [nodes, setNodes, onNodesChange] = useNodesState([]) + const [edges, setEdges, onEdgesChange] = useEdgesState([]) + const [isDarkMode, setIsDarkMode] = useState(() => { const saved = localStorage.getItem('darkMode') return saved ? JSON.parse(saved) : false }) - const [gridSize, setGridSize] = useState({ width: 0, height: 0 }) - const gridContainerRef = useRef(null) - const [editingDevice, setEditingDevice] = useState(null) - const [connections, setConnections] = useState([]) - const [connectingFrom, setConnectingFrom] = useState<{ - deviceId: string - portId: string - } | null>(null) - const [tempConnection, setTempConnection] = useState<{ - x: number - y: number - } | null>(null) + + useEffect(() => { + localStorage.setItem('darkMode', JSON.stringify(isDarkMode)) + if (isDarkMode) { + document.documentElement.classList.add('dark') + } else { + document.documentElement.classList.remove('dark') + } + }, [isDarkMode]) + + useEffect(() => { + if (devices) { + const flowNodes = devices.map(device => ({ + id: device.id, + type: 'customNode', + position: device.position, + data: { + ...device, + onEdit: handleEditDevice, + onDelete: handleDeleteDevice, + }, + })) + setNodes(flowNodes) + } + }, [devices, setNodes]) + + const defaultEdgeOptions = useMemo( + () => ({ + style: { + strokeWidth: 2, + stroke: isDarkMode ? '#a0aec0' : '#4a5568', + }, + animated: true, + }), + [isDarkMode], + ) + + useEffect(() => { + setEdges(eds => + eds.map(edge => ({ + ...edge, + style: { + ...edge.style, + stroke: isDarkMode ? '#a0aec0' : '#4a5568', + }, + })), + ) + }, [isDarkMode, setEdges]) + + const onConnect = useCallback( + (params: Edge | Connection) => { + const newEdge = { + ...params, + animated: true, + style: { stroke: isDarkMode ? '#a0aec0' : '#4a5568', strokeWidth: 2 }, + } + setEdges(eds => addEdge(newEdge, eds)) + addConnection({ + id: `${params.source}-${params.target}`, + sourceDeviceId: params.source as string, + sourcePortId: params.sourceHandle as string, + targetDeviceId: params.target as string, + targetPortId: params.targetHandle as string, + }) + }, + [setEdges, addConnection, isDarkMode], + ) const handleAddCustomDevice = (deviceData: Omit) => { - addDevice({ + const newDevice = { ...deviceData, id: `device-${Date.now()}`, - }) + position: { x: 100, y: 100 }, + } + addDevice(newDevice) } const handleEditDevice = (device: Device) => { @@ -62,91 +139,14 @@ function AudioDeviceArrangerApp() { const handleDeleteDevice = (id: string) => { deleteDevice(id) - setConnections( - connections.filter( - conn => conn.sourceDeviceId !== id && conn.targetDeviceId !== id, - ), + setNodes(nds => nds.filter(node => node.id !== id)) + setEdges(eds => + eds.filter(edge => edge.source !== id && edge.target !== id), ) } - const handlePortClick = ( - deviceId: string, - portId: string, - isOutput: boolean, - event: React.MouseEvent, - ) => { - if (connectingFrom) { - if (isOutput || connectingFrom.deviceId === deviceId) { - // Can't connect output to output or to the same device - setConnectingFrom(null) - setTempConnection(null) - return - } - // Complete the connection - const newConnection: Connection = { - id: `connection-${Date.now()}`, - sourceDeviceId: connectingFrom.deviceId, - sourcePortId: connectingFrom.portId, - targetDeviceId: deviceId, - targetPortId: portId, - } - setConnections([...connections, newConnection]) - setConnectingFrom(null) - setTempConnection(null) - } else if (isOutput) { - // Start a new connection from an output port - setConnectingFrom({ deviceId, portId }) - setTempConnection({ x: event.clientX, y: event.clientY }) - } - } - - useEffect(() => { - localStorage.setItem('darkMode', JSON.stringify(isDarkMode)) - if (isDarkMode) { - document.documentElement.classList.add('dark') - } else { - document.documentElement.classList.remove('dark') - } - }, [isDarkMode]) - - useEffect(() => { - const updateGridSize = () => { - if (gridContainerRef.current) { - const toolbarWidth = 256 // Adjust if needed (w-64 = 16rem = 256px) - setGridSize({ - width: window.innerWidth - toolbarWidth, - height: gridContainerRef.current.clientHeight, - }) - } - } - - updateGridSize() - window.addEventListener('resize', updateGridSize) - return () => window.removeEventListener('resize', updateGridSize) - }, []) - const toggleDarkMode = () => setIsDarkMode(!isDarkMode) - const handleGridExpansion = (newPosition: { x: number; y: number }) => { - setGridSize(prevSize => ({ - width: Math.max(prevSize.width, newPosition.x + GRID_SIZE), - height: Math.max(prevSize.height, newPosition.y + GRID_SIZE), - })) - } - - const handleMouseMove = (event: React.MouseEvent) => { - if (connectingFrom) { - setTempConnection({ x: event.clientX, y: event.clientY }) - } - } - - const handleMouseUp = () => { - if (connectingFrom) { - setConnectingFrom(null) - setTempConnection(null) - } - } - if (isLoading) return (
@@ -154,20 +154,8 @@ function AudioDeviceArrangerApp() {
) - const dotMatrixStyle = { - backgroundImage: ` - radial-gradient(circle, ${isDarkMode ? '#4a5568' : '#e0e0e0'} ${ - DOT_SIZE / 2 - }px, transparent ${DOT_SIZE / 2}px) - `, - backgroundSize: `${GRID_SIZE}px ${GRID_SIZE}px`, - backgroundPosition: `-${DOT_SIZE / 2}px -${DOT_SIZE / 2}px`, - width: `${Math.max(gridSize.width, window.innerWidth - 256)}px`, - height: `${Math.max(gridSize.height, window.innerHeight - 64)}px`, - } - return ( -
+
{/* Toolbar */} - {/* Grid */} -
-
+ - {connections.map(connection => { - const sourceDevice = devices.find( - d => d.id === connection.sourceDeviceId, - ) - const targetDevice = devices.find( - d => d.id === connection.targetDeviceId, - ) - if (!sourceDevice || !targetDevice) return null - - return ( - - ) - })} - {connectingFrom && tempConnection && ( - d.id === connectingFrom.deviceId)!.position - .x + - GRID_SIZE * 1.5 - } - startY={ - devices.find(d => d.id === connectingFrom.deviceId)!.position - .y + - GRID_SIZE * 1.5 - } - endX={tempConnection.x} - endY={tempConnection.y} - isTemp={true} - /> - )} - {devices.map(device => ( - { - updateDevicePosition({ id, position }) - handleGridExpansion(position) - }} - onEdit={handleEditDevice} - onDelete={handleDeleteDevice} - onPortClick={handlePortClick} - isConnecting={!!connectingFrom} - gridWidth={gridSize.width} - gridHeight={gridSize.height} - /> - ))} -
+ + + +
diff --git a/src/components/ConnectionLine.tsx b/src/components/ConnectionLine.tsx deleted file mode 100644 index afe4b8c..0000000 --- a/src/components/ConnectionLine.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// src/components/ConnectionLine.tsx - -import React from 'react' - -interface ConnectionLineProps { - startX: number - startY: number - endX: number - endY: number - isTemp?: boolean -} - -const ConnectionLine: React.FC = ({ - startX, - startY, - endX, - endY, - isTemp = false, -}) => { - // Calculate control points for a quadratic bezier curve - const midX = (startX + endX) / 2 - const midY = (startY + endY) / 2 - const controlX = midX - const controlY = midY - Math.abs(endY - startY) / 2 - - return ( - - - - ) -} - -export default ConnectionLine diff --git a/src/components/CustomNode.tsx b/src/components/CustomNode.tsx new file mode 100644 index 0000000..280ccda --- /dev/null +++ b/src/components/CustomNode.tsx @@ -0,0 +1,65 @@ +// src/components/CustomNode.tsx + +import React from 'react' +import { Handle, Position } from '@xyflow/react' +import { Device, Port } from '../types/devices' + +interface CustomNodeData extends Device { + onEdit: (device: Device) => void + onDelete: (id: string) => void +} + +const CustomNode: React.FC<{ data: CustomNodeData }> = ({ data }) => { + return ( +
+
+
+ {data.type.charAt(0).toUpperCase()} +
+
+
+ {data.name} +
+
{data.type}
+
+
+ + {data.inputs.map((input: Port, index: number) => ( + + ))} + + {data.outputs.map((output: Port, index: number) => ( + + ))} + +
+ + +
+
+ ) +} + +export default CustomNode diff --git a/src/components/DeviceNode.tsx b/src/components/DeviceNode.tsx deleted file mode 100644 index bd5b90b..0000000 --- a/src/components/DeviceNode.tsx +++ /dev/null @@ -1,205 +0,0 @@ -// src/components/DeviceNode.tsx - -import React, { useRef, useEffect, useState, useCallback } from 'react' -import * as Tooltip from '@radix-ui/react-tooltip' -import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import { Device, Port } from '../types/devices' - -interface DeviceNodeProps { - device: Device - onMove: (id: string, position: { x: number; y: number }) => void - onEdit: (device: Device) => void - onDelete: (id: string) => void - onPortClick: ( - deviceId: string, - portId: string, - isOutput: boolean, - event: React.MouseEvent, - ) => void - isConnecting: boolean - gridWidth: number - gridHeight: number -} - -const DeviceNode: React.FC = ({ - device, - onMove, - onEdit, - onDelete, - onPortClick, - isConnecting, - gridWidth, - gridHeight, -}) => { - const nodeRef = useRef(null) - const [position, setPosition] = useState(device.position) - const [isDragging, setIsDragging] = useState(false) - const dragStartRef = useRef({ x: 0, y: 0 }) - const [nodeSize, setNodeSize] = useState({ width: 0, height: 0 }) - - console.log('inputs', gridWidth, gridHeight, nodeSize) - - useEffect(() => { - setPosition(device.position) - }, [device.position]) - - useEffect(() => { - if (nodeRef.current) { - setNodeSize({ - width: nodeRef.current.offsetWidth, - height: nodeRef.current.offsetHeight, - }) - } - }, []) - - const snapToGrid = useCallback( - (x: number, y: number) => { - const snappedX = Math.round(x / device.gridSize) * device.gridSize - const snappedY = Math.round(y / device.gridSize) * device.gridSize - return { x: snappedX, y: snappedY } - }, - [device.gridSize], - ) - - useEffect(() => { - const handleMouseMove = (e: MouseEvent) => { - if (!isDragging) return - - const dx = e.clientX - dragStartRef.current.x - const dy = e.clientY - dragStartRef.current.y - - setPosition(prevPos => ({ - x: prevPos.x + dx, - y: prevPos.y + dy, - })) - - dragStartRef.current = { x: e.clientX, y: e.clientY } - } - - const handleMouseUp = () => { - if (isDragging) { - setIsDragging(false) - const snappedPosition = snapToGrid(position.x, position.y) - setPosition(snappedPosition) - onMove(device.id, snappedPosition) - } - } - - if (isDragging) { - window.addEventListener('mousemove', handleMouseMove) - window.addEventListener('mouseup', handleMouseUp) - } - - return () => { - window.removeEventListener('mousemove', handleMouseMove) - window.removeEventListener('mouseup', handleMouseUp) - } - }, [isDragging, device.id, onMove, position, snapToGrid]) - - const handleMouseDown = (e: React.MouseEvent) => { - e.preventDefault() - setIsDragging(true) - dragStartRef.current = { x: e.clientX, y: e.clientY } - } - - const renderPorts = (ports: Port[], isOutput: boolean) => ( -
-

- {isOutput ? 'Outputs' : 'Inputs'} -

- {ports.map(port => ( -
{ - e.stopPropagation() - onPortClick(device.id, port.id, isOutput, e) - }} - > - {port.name} -
- ))} -
- ) - - return ( - - - -
-

- {device.name} -

-
- Type: {device.type} -
- {renderPorts(device.inputs, false)} - {renderPorts(device.outputs, true)} -
- Grid: ({Math.round(position.x / device.gridSize)},{' '} - {Math.round(position.y / device.gridSize)}) -
- - - - - - - onEdit(device)} - > - Edit - - onDelete(device.id)} - > - Delete - - - - -
-
- - -

Inputs: {device.inputs.length}

-

Outputs: {device.outputs.length}

- -
-
-
-
- ) -} - -export default DeviceNode diff --git a/src/hooks/useAudioDeviceStore.ts b/src/hooks/useAudioDeviceStore.ts index 9f14d95..4e12604 100644 --- a/src/hooks/useAudioDeviceStore.ts +++ b/src/hooks/useAudioDeviceStore.ts @@ -117,7 +117,6 @@ export function useAudioDeviceStore() { return { previousDevices } }, onError: (err, newTodo, context) => { - console.log('Error updating device position', err, newTodo, context) queryClient.setQueryData(DEVICES_KEY, context?.previousDevices) }, onSettled: () => { diff --git a/src/reactflow-dark.css b/src/reactflow-dark.css new file mode 100644 index 0000000..e3851c7 --- /dev/null +++ b/src/reactflow-dark.css @@ -0,0 +1,41 @@ +/* src/reactflow-dark.css */ + +.dark .react-flow__node { + background-color: #2d3748; + color: #e2e8f0; +} + +.dark .react-flow__handle { + background-color: #4a5568; +} + +.dark .react-flow__edge-path { + stroke: #718096; +} + +.dark-flow { + background-color: #1a202c; +} + +.dark-flow .react-flow__controls { + background-color: #2d3748; +} + +.dark-flow .react-flow__controls-button { + background-color: #4a5568; + color: #e2e8f0; + border-bottom: 1px solid #718096; +} + +.dark-minimap { + background-color: #2d3748; +} + +.dark-minimap .react-flow__minimap-mask { + fill: #4a5568; +} + +.dark-minimap .react-flow__minimap-node { + fill: #718096; + stroke: #a0aec0; +}