An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
at new-directions 119 lines 3.0 kB view raw
1<script lang="ts"> 2 /** 3 * CytoscapeGraph wrapper component. 4 * A reusable Svelte 5 wrapper around Cytoscape.js with dagre layout support. 5 * Handles initialization, reactive updates, cleanup, and exposes methods for external control. 6 */ 7 8 import { onMount } from 'svelte'; 9 import cytoscape from 'cytoscape'; 10 import type { ElementDefinition, Stylesheet } from 'cytoscape'; 11 import dagre from 'cytoscape-dagre'; 12 13 // Register the dagre layout extension at module level, once 14 cytoscape.use(dagre); 15 16 type Props = { 17 elements: Array<ElementDefinition>; 18 stylesheet: Array<Stylesheet>; 19 layoutOptions?: Record<string, unknown>; 20 onNodeClick?: (nodeId: string) => void; 21 onBackgroundClick?: () => void; 22 }; 23 24 let { 25 elements, 26 stylesheet, 27 layoutOptions = { name: 'dagre', rankDir: 'TB', spacingFactor: 1.5 }, 28 onNodeClick, 29 onBackgroundClick, 30 }: Props = $props(); 31 32 let containerDiv: HTMLDivElement | undefined = $state(); 33 let cyInstance: cytoscape.Core | null = $state(null); 34 let initialized = $state(false); 35 36 onMount(() => { 37 if (!containerDiv) return; 38 39 // Initialize cytoscape instance 40 cyInstance = cytoscape({ 41 container: containerDiv, 42 elements: elements, 43 style: stylesheet, 44 layout: layoutOptions as cytoscape.LayoutOptions, 45 pixelRatio: 1, 46 wheelSensitivity: 0.2, 47 }); 48 49 // Register event handlers 50 cyInstance.on('tap', 'node', (event: cytoscape.EventObject) => { 51 const nodeId = event.target.id(); 52 onNodeClick?.(nodeId); 53 }); 54 55 cyInstance.on('tap', (event: cytoscape.EventObject) => { 56 // Only trigger background click if target is the background (not a node/edge) 57 if (event.target === cyInstance) { 58 onBackgroundClick?.(); 59 } 60 }); 61 62 initialized = true; 63 64 // Return cleanup function 65 return () => { 66 if (cyInstance) { 67 cyInstance.destroy(); 68 cyInstance = null; 69 } 70 }; 71 }); 72 73 // Reactive updates when elements change 74 $effect(() => { 75 if (!cyInstance || !initialized) return; 76 77 cyInstance.batch(() => { 78 cyInstance!.elements().remove(); 79 cyInstance!.add(elements); 80 }); 81 82 // Re-run layout after adding elements 83 const layout = cyInstance.layout(layoutOptions as cytoscape.LayoutOptions); 84 layout.run(); 85 }); 86 87 // Reactive updates when stylesheet changes 88 $effect(() => { 89 if (!cyInstance) return; 90 cyInstance.style(stylesheet); 91 }); 92 93 /** 94 * Fit the entire graph to the viewport with padding. 95 */ 96 export function fitView(): void { 97 if (cyInstance) { 98 cyInstance.fit(undefined, 20); 99 } 100 } 101 102 /** 103 * Get the underlying Cytoscape instance for advanced use cases. 104 */ 105 export function getInstance(): cytoscape.Core | null { 106 return cyInstance; 107 } 108</script> 109 110<div class="cytoscape-container" bind:this={containerDiv}></div> 111 112<style> 113 .cytoscape-container { 114 width: 100%; 115 height: 100%; 116 background-color: #f5f5f5; 117 position: relative; 118 } 119</style>