diff --git a/dnd-ui/webview-ui/src/App.tsx b/dnd-ui/webview-ui/src/App.tsx index f6ca17f..61445ee 100644 --- a/dnd-ui/webview-ui/src/App.tsx +++ b/dnd-ui/webview-ui/src/App.tsx @@ -6,6 +6,7 @@ import { Provider } from './context/store'; import InitGlobalState from './InitGlobalState'; import { ToastContainer } from 'react-toastify'; import 'vite/modulepreload-polyfill'; +import GroupNodeProvider from './context/CollapsibleContext'; declare global { interface Window { @@ -18,12 +19,14 @@ declare global { function App() { return ( - - - - - - + + + + + + + + ); } diff --git a/dnd-ui/webview-ui/src/components/CollabPanel.tsx b/dnd-ui/webview-ui/src/components/CollabPanel.tsx deleted file mode 100644 index 10f45fa..0000000 --- a/dnd-ui/webview-ui/src/components/CollabPanel.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// @ts-nocheck -import { useState } from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faHandshake } from '@fortawesome/free-regular-svg-icons'; -import { memo } from 'react'; -import { Panel } from 'reactflow'; -import { getWebSocket } from '../socket/getWebSocket'; -console.log('getWebSocket:', getWebSocket); - -function SavePanel() { - const [isCollab, setCollab] = useState(true); - const style = 'rounded !text-white font-semibold py-2 px-5 cursor-pointer'; - - function handleCollab(newStt) { - setCollab(newStt); - const ws = getWebSocket(); - if (newStt) { - ws.connect(); - } else { - ws.destroy(); - } - } - - return ( - handleCollab(!isCollab)} - > - - {isCollab ? ( - - ) : ( - - )} - - - ); -} - -export default memo(SavePanel); diff --git a/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx b/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx index 38fa450..7f40224 100644 --- a/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx +++ b/dnd-ui/webview-ui/src/components/NodeMenuPanel.tsx @@ -1,12 +1,13 @@ // @ts-nocheck -import { Dispatch, memo, SetStateAction, useRef } from 'react'; +import { faAngleDown, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Dispatch, memo, SetStateAction, useRef } from 'react'; import { Panel } from 'reactflow'; -import defaultNodes from '../nodes/defaultNode.json'; -import { RFNode } from '../models/nodeFactory'; +import { useCollapsibleNodes } from '../context/CollapsibleContext'; import { useStore } from '../context/store'; import { useStaticClickAway } from '../hooks/useClickOutside'; -import { faAngleDown, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { RFNode } from '../models/nodeFactory'; +import defaultNodes from '../nodes/defaultNode.json'; interface INodeMenuPanel { setShowMenu: Dispatch>; @@ -23,6 +24,7 @@ function NodeMenuPanel({ }: INodeMenuPanel) { const nodeMenuRef = useRef(null); const addNodeRef = useRef(null); + const { setGroupNodes } = useCollapsibleNodes(); const [takeSnapshot] = useStore((store) => store.takeSnapshot); useStaticClickAway(nodeMenuRef, () => setShowMenu(false), addNodeRef); @@ -38,6 +40,15 @@ function NodeMenuPanel({ }, position: { x: event.clientX - curX + 20, y: event.clientY - curY + 20 }, }); + + if (newNode.type === 'group') { + const { width, height } = newNode; + + setGroupNodes((prev) => { + return { ...prev, [newNode.id]: { width, height } }; + }); + } + await setNodes((prev) => [...prev, newNode]); setShowMenu(false); takeSnapshot(); @@ -78,6 +89,7 @@ function NodeMenuPanel({ > +
+ +
{/*
+ + {hasChildNodes && ( + + )} + +
+ ); +} + +type IsEqualCompareObj = { + minWidth: number; + minHeight: number; + hasChildNodes: boolean; +}; + +function isEqual(prev: IsEqualCompareObj, next: IsEqualCompareObj): boolean { + return ( + prev.minWidth === next.minWidth && + prev.minHeight === next.minHeight && + prev.hasChildNodes === next.hasChildNodes + ); +} + +export default memo(GroupNode); diff --git a/dnd-ui/webview-ui/src/pages/Flow.css b/dnd-ui/webview-ui/src/pages/Flow.css index a359482..5ca8aa8 100644 --- a/dnd-ui/webview-ui/src/pages/Flow.css +++ b/dnd-ui/webview-ui/src/pages/Flow.css @@ -1,4 +1,4 @@ .selected { - box-shadow: 0 0 2px 3px #ff0071; + box-shadow: 0 0 2px 3px #ff0071 !important; border-radius: 0.5rem; } diff --git a/dnd-ui/webview-ui/src/pages/Flow.tsx b/dnd-ui/webview-ui/src/pages/Flow.tsx index d9a8d0a..922c1c8 100644 --- a/dnd-ui/webview-ui/src/pages/Flow.tsx +++ b/dnd-ui/webview-ui/src/pages/Flow.tsx @@ -4,7 +4,13 @@ import './flow.css'; import get from 'lodash/get'; import omit from 'lodash/omit'; import pick from 'lodash/pick'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { + DragEventHandler, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import ReactFlow, { Background, Connection, @@ -14,6 +20,7 @@ import ReactFlow, { MiniMap, Node, NodeChange, + NodeDragHandler, OnNodesChange, OnSelectionChangeParams, Viewport, @@ -22,33 +29,41 @@ import ReactFlow, { updateEdge, useEdgesState, useNodesState, + useReactFlow, + useStoreApi, } from 'reactflow'; import YAML from 'yaml'; import { toast } from 'react-toastify'; import { Compiler } from '../compilers'; -import CollabPanel from '../components/CollabPanel'; import CompilePanel from '../components/CompilePanel'; import CustomEdge from '../components/CustomEdge'; +import HelperLines from '../components/HelperLine'; import MenuPanel from '../components/MenuPanel'; import NodeMenuPanel from '../components/NodeMenuPanel'; import SavePanel from '../components/SavePanel'; import { useStore } from '../context/store'; +import useCopyPaste from '../hooks/useCopyPaste'; +import useExpandCollapse from '../hooks/useExpandCollapse'; import { useGetWindowSize } from '../hooks/useGetWindowSize'; import useUndoRedo from '../hooks/useUndoRedo'; import AnyNode from '../nodes/AnyNode'; import CTFlowRecorderNode from '../nodes/CTFlowRecorderNode'; import CustomNodeRender from '../nodes/CustomNodeRender'; +import GroupNode from '../nodes/GroupNode'; import ButtonNode from '../nodes/old_nodes/ButtonNode'; import CheckboxNode from '../nodes/old_nodes/CheckboxNode'; import ContainsNode from '../nodes/old_nodes/ContainsNode'; import TextInputNode from '../nodes/old_nodes/TextInputNode'; import VisitPageNode from '../nodes/old_nodes/VisitPageNode'; import WaitNode from '../nodes/old_nodes/WaitNode'; +import { + getId, + getNodePositionInsideParent, + sortNodes, +} from '../utilities/groupNodes'; import { getHelperLines } from '../utilities/helperLines'; import { vscode } from '../utilities/vscode'; -import HelperLines from '../components/HelperLine'; -import useCopyPaste from '../hooks/useCopyPaste'; const fitViewOptions: FitViewOptions = { padding: 0.2, @@ -70,6 +85,7 @@ const nodeTypes = { CTFlowRecorderNode: CTFlowRecorderNode, customNode: CustomNodeRender, anyNode: AnyNode, + group: GroupNode, // TODO: remove old nodes in new version buttonNode: ButtonNode, @@ -87,9 +103,14 @@ const edgeTypes = { const Editor = () => { const { height: windowHeight, width: windowWidth } = useGetWindowSize(); const { takeSnapshot, undo, redo, setPast } = useUndoRedo(); + const wrapperRef = useRef(null); const [nodes, setNodes] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + const { nodes: visibleNodes, edges: visibleEdges } = useExpandCollapse( + nodes, + edges + ); const [, setSelectedNode] = useState(null); const [store, setStore] = useStore((store) => store); const [showMenu, setShowMenu] = useState(false); @@ -104,6 +125,8 @@ const Editor = () => { const canPaste = bufferedNodes.length > 0; const viewport = useRef(defaultViewport); + const { project, getIntersectingNodes } = useReactFlow(); + const rfStore = useStoreApi(); useEffect(() => { window.addEventListener('message', handleCallback); @@ -238,19 +261,6 @@ const Editor = () => { setStore({ ...payload }); const yamlData = YAML.stringify(payload); - // When testing on browser, the vscode.postMessage won't work - // we will manually emit fileUpdate event - // if (window) { - // const simulateFileUpdateTriggerOnBrowser = new CustomEvent("message", { - // detail: { - // "type": 'fileUpdate', - // "text": yamlData, - // }, - // }); - - // // window.dispatchEvent(simulateFileUpdateTriggerOnBrowser) - // } - vscode.postMessage({ type: 'addEdit', data: { @@ -279,10 +289,6 @@ const Editor = () => { viewport.current = curViewport; } - const dragStop = useCallback(() => { - takeSnapshot(); - }, [takeSnapshot]); - const customApplyNodeChanges = useCallback( (changes: NodeChange[], nodes: Node[]): Node[] => { // reset the helper lines (clear existing lines, if any) @@ -323,17 +329,163 @@ const Editor = () => { [setNodes, customApplyNodeChanges] ); + const onNodeDrag: NodeDragHandler = useCallback( + (_: React.MouseEvent, node: Node) => { + if (node.type !== 'anyNode' && !node.parentNode) { + return; + } + + const intersections = getIntersectingNodes(node).filter( + (n) => n.type === 'group' + ); + const groupClassName = + intersections.length && node.parentNode !== intersections[0]?.id + ? 'active' + : ''; + + setNodes((nds) => { + return nds.map((n) => { + if (n.type === 'group') { + return { + ...n, + className: groupClassName, + }; + } else if (n.id === node.id) { + return { + ...n, + position: node.position, + }; + } + + return { ...n }; + }); + }); + }, + [getIntersectingNodes, setNodes] + ); + const onNodeDragStop = useCallback( + (_: React.MouseEvent, node: Node) => { + if (node.type !== 'anyNode' && !node.parentNode) { + return; + } + + const intersections = getIntersectingNodes(node).filter( + (n) => n.type === 'group' + ); + const groupNode = intersections[0]; + + // when there is an intersection on drag stop, we want to attach the node to its new parent + if (intersections.length && node.parentNode !== groupNode?.id) { + const nextNodes: Node[] = rfStore + .getState() + .getNodes() + .map((n) => { + if (n.id === groupNode.id) { + return { + ...n, + className: '', + }; + } else if (n.id === node.id) { + const position = getNodePositionInsideParent(n, groupNode) ?? { + x: 0, + y: 0, + }; + + return { + ...n, + position, + parentNode: groupNode.id, + // we need to set dragging = false, because the internal change of the dragging state + // is not applied yet, so the node would be rendered as dragging + dragging: false, + extent: 'parent' as 'parent', + }; + } + + return n; + }) + .sort(sortNodes); + + setNodes(nextNodes); + } + takeSnapshot(); + }, + [getIntersectingNodes, setNodes, takeSnapshot] + ); + const onDrop: DragEventHandler = (event: React.DragEvent) => { + event.preventDefault(); + + if (wrapperRef.current) { + const wrapperBounds = wrapperRef.current.getBoundingClientRect(); + const type = event.dataTransfer.getData('application/reactflow'); + const position = project({ + x: event.clientX - wrapperBounds.x - 20, + y: event.clientY - wrapperBounds.top - 20, + }); + console.log('position:', position); + const nodeStyle = + type === 'group' ? { width: 400, height: 200 } : undefined; + + const intersections = getIntersectingNodes({ + x: position.x, + y: position.y, + width: 40, + height: 40, + }).filter((n) => n.type === 'group'); + const groupNode = intersections[0]; + + const newNode: Node = { + id: getId(), + type, + position, + data: { label: `${type}` }, + style: nodeStyle, + }; + + if (groupNode) { + // if we drop a node on a group node, we want to position the node inside the group + newNode.position = getNodePositionInsideParent( + { + position, + width: 40, + height: 40, + }, + groupNode + ) ?? { x: 0, y: 0 }; + newNode.parentNode = groupNode?.id; + newNode.extent = groupNode ? 'parent' : undefined; + } + + // we need to make sure that the parents are sorted before the children + // to make sure that the children are rendered on top of the parents + const sortedNodes = rfStore + .getState() + .getNodes() + .concat(newNode) + .sort(sortNodes); + setNodes(sortedNodes); + } + }; + const onDragOver = (event: React.DragEvent) => { + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + }; return ( -
+
{ - { + if (a.type === b.type) { + return 0; + } + return a.type === 'group' && b.type !== 'group' ? -1 : 1; +}; + +export const getId = (prefix = 'node') => `${prefix}_${Math.random() * 10000}`; + +export const getNodePositionInsideParent = ( + node: Partial, + groupNode: Node +) => { + const position = node.position ?? { x: 0, y: 0 }; + const nodeWidth = node.width ?? 0; + const nodeHeight = node.height ?? 0; + const groupWidth = groupNode.width ?? 0; + const groupHeight = groupNode.height ?? 0; + + if (position.x < groupNode.position.x) { + position.x = 0; + } else if (position.x + nodeWidth > groupNode.position.x + groupWidth) { + position.x = groupWidth - nodeWidth; + } else { + position.x = position.x - groupNode.position.x; + } + + if (position.y < groupNode.position.y) { + position.y = 0; + } else if (position.y + nodeHeight > groupNode.position.y + groupHeight) { + position.y = groupHeight - nodeHeight; + } else { + position.y = position.y - groupNode.position.y; + } + + return position; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..04fe730 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@reactflow/node-resizer": "^2.2.6" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..708176f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,365 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@reactflow/node-resizer': + specifier: ^2.2.6 + version: 2.2.6(react-dom@18.2.0)(react@18.2.0) + +packages: + + /@reactflow/core@11.10.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-GIh3usY1W3eVobx//OO9+Cwm+5evQBBdPGxDaeXwm25UqPMWRI240nXQA5F/5gL5Mwpf0DUC7DR2EmrKNQy+Rw==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + dependencies: + '@types/d3': 7.4.3 + '@types/d3-drag': 3.0.7 + '@types/d3-selection': 3.0.10 + '@types/d3-zoom': 3.0.8 + classcat: 5.0.4 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + zustand: 4.4.6(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + dev: false + + /@reactflow/node-resizer@2.2.6(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-1Xb6q97uP7hRBLpog9sRCNfnsHdDgFRGEiU+lQqGgPEAeYwl4nRjWa/sXwH6ajniKxBhGEvrdzOgEFn6CRMcpQ==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + dependencies: + '@reactflow/core': 11.10.1(react-dom@18.2.0)(react@18.2.0) + classcat: 5.0.4 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + zustand: 4.4.6(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + dev: false + + /@types/d3-array@3.2.1: + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + dev: false + + /@types/d3-axis@3.0.6: + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-brush@3.0.6: + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-chord@3.0.6: + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + dev: false + + /@types/d3-color@3.1.3: + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + dev: false + + /@types/d3-contour@3.0.6: + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + dependencies: + '@types/d3-array': 3.2.1 + '@types/geojson': 7946.0.13 + dev: false + + /@types/d3-delaunay@6.0.4: + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + dev: false + + /@types/d3-dispatch@3.0.6: + resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==} + dev: false + + /@types/d3-drag@3.0.7: + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-dsv@3.0.7: + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + dev: false + + /@types/d3-ease@3.0.2: + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + dev: false + + /@types/d3-fetch@3.0.7: + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + dependencies: + '@types/d3-dsv': 3.0.7 + dev: false + + /@types/d3-force@3.0.9: + resolution: {integrity: sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==} + dev: false + + /@types/d3-format@3.0.4: + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + dev: false + + /@types/d3-geo@3.1.0: + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + dependencies: + '@types/geojson': 7946.0.13 + dev: false + + /@types/d3-hierarchy@3.1.6: + resolution: {integrity: sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw==} + dev: false + + /@types/d3-interpolate@3.0.4: + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + dependencies: + '@types/d3-color': 3.1.3 + dev: false + + /@types/d3-path@3.0.2: + resolution: {integrity: sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==} + dev: false + + /@types/d3-polygon@3.0.2: + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + dev: false + + /@types/d3-quadtree@3.0.5: + resolution: {integrity: sha512-Cb1f3jyNBnvMMkf4KBZ7IgAQVWd9yzBwYcrxGqg3aPCUgWELAS+nyeB7r76aqu1e3+CGDjhk4BrWaFBekMwigg==} + dev: false + + /@types/d3-random@3.0.3: + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + dev: false + + /@types/d3-scale-chromatic@3.0.2: + resolution: {integrity: sha512-kpKNZMDT3OAX6b5ct5nS/mv6LULagnUy4DmS6yyNjclje1qVe7vbjPwY3q1TGz6+Wr2IUkgFatCzqYUl54fHag==} + dev: false + + /@types/d3-scale@4.0.8: + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + dependencies: + '@types/d3-time': 3.0.3 + dev: false + + /@types/d3-selection@3.0.10: + resolution: {integrity: sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==} + dev: false + + /@types/d3-shape@3.1.5: + resolution: {integrity: sha512-dfEWpZJ1Pdg8meLlICX1M3WBIpxnaH2eQV2eY43Y5ysRJOTAV9f3/R++lgJKFstfrEOE2zdJ0sv5qwr2Bkic6Q==} + dependencies: + '@types/d3-path': 3.0.2 + dev: false + + /@types/d3-time-format@4.0.3: + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + dev: false + + /@types/d3-time@3.0.3: + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + dev: false + + /@types/d3-timer@3.0.2: + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + dev: false + + /@types/d3-transition@3.0.8: + resolution: {integrity: sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==} + dependencies: + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3-zoom@3.0.8: + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.10 + dev: false + + /@types/d3@7.4.3: + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.6 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.9 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.6 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.0.2 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.5 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.8 + '@types/d3-scale-chromatic': 3.0.2 + '@types/d3-selection': 3.0.10 + '@types/d3-shape': 3.1.5 + '@types/d3-time': 3.0.3 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.8 + '@types/d3-zoom': 3.0.8 + dev: false + + /@types/geojson@7946.0.13: + resolution: {integrity: sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==} + dev: false + + /classcat@5.0.4: + resolution: {integrity: sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==} + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + dev: false + + /d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + + /d3-transition@3.0.1(d3-selection@3.0.0): + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + dev: false + + /d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /zustand@4.4.6(react@18.2.0): + resolution: {integrity: sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false