import cytoscape from 'cytoscape';
import type { GraphData, LayoutType } from '../types';
export function initializeCytoscape(
container: HTMLElement,
graphData: GraphData,
onNodeSelect: (nodeData: any) => void,
onBackgroundClick: () => void
) {
const elements: any[] = [];
// Add nodes
for (const node of graphData.nodes) {
elements.push({
group: 'nodes',
data: {
id: node.id,
label: node.label,
type: node.type,
description: node.description,
metadata: node.metadata,
changeStatus: node.changeStatus || 'unchanged',
changeDetails: node.changeDetails || null,
usesUserContext: node.metadata?.usesUserContext || false,
usesOrganizationContext: node.metadata?.usesOrganizationContext || false,
isReadOnly: node.metadata?.isReadOnly,
},
});
}
// Add edges
for (const edge of graphData.edges) {
elements.push({
group: 'edges',
data: {
id: edge.id,
source: edge.source,
target: edge.target,
type: edge.type,
label: edge.label || '',
},
});
}
const cy = cytoscape({
container,
elements,
style: getCytoscapeStyles(),
layout: {
name: 'cose',
animate: false,
nodeRepulsion: 8000,
idealEdgeLength: 100,
edgeElasticity: 100,
gravity: 0.25,
numIter: 1000,
padding: 50,
},
minZoom: 0.2,
maxZoom: 3,
wheelSensitivity: 0.3,
});
// Node click handler
cy.on('tap', 'node', function(evt: any) {
const node = evt.target;
onNodeSelect(node);
});
// Background click handler
cy.on('tap', function(evt: any) {
if (evt.target === cy) {
onBackgroundClick();
}
});
return cy;
}
export function getCytoscapeStyles() {
return [
// Base node style
{
selector: 'node',
style: {
'label': 'data(label)',
'text-valign': 'center',
'text-halign': 'center',
'font-family': 'Space Grotesk, -apple-system, BlinkMacSystemFont, sans-serif',
'font-size': 12,
'font-weight': 500,
'color': '#023d60',
'text-outline-width': 2,
'text-outline-color': '#ffffff',
'background-color': '#ffffff',
'border-width': 2,
'width': 90,
'height': 45,
},
},
// Function nodes - Navy
{
selector: 'node[type="function"]',
style: {
'shape': 'round-hexagon',
'border-color': '#023d60',
'background-color': 'rgba(2, 61, 96, 0.08)',
'width': 100,
'height': 55,
},
},
// Function nodes that are mutations (isReadOnly: false) - Orange accent
{
selector: 'node[type="function"][!isReadOnly]',
style: {
'border-color': '#fe5d26',
'background-color': 'rgba(254, 93, 38, 0.08)',
},
},
// Mutation function nodes - show edit indicator (when no userContext)
{
selector: 'node[type="function"][!isReadOnly]:not([?usesUserContext])',
style: {
'background-image': 'data:image/svg+xml,' + encodeURIComponent(''),
'background-width': '18px',
'background-height': '18px',
'background-position-x': '50%',
'background-position-y': '75%',
'text-valign': 'center',
'text-margin-y': -8,
},
},
// Function nodes with userContext (read-only) - show indicator below label
{
selector: 'node[type="function"][?usesUserContext][?isReadOnly]',
style: {
'background-image': 'data:image/svg+xml,' + encodeURIComponent(''),
'background-width': '18px',
'background-height': '18px',
'background-position-x': '50%',
'background-position-y': '75%',
'text-valign': 'center',
'text-margin-y': -8,
},
},
// Function nodes with BOTH mutation AND userContext - show both icons
{
selector: 'node[type="function"][!isReadOnly][?usesUserContext]',
style: {
'border-color': '#fe5d26',
'background-color': 'rgba(254, 93, 38, 0.08)',
'background-image': [
'data:image/svg+xml,' + encodeURIComponent(''),
'data:image/svg+xml,' + encodeURIComponent('')
],
'background-width': ['16px', '16px'],
'background-height': ['16px', '16px'],
'background-position-x': ['35%', '65%'],
'background-position-y': ['75%', '75%'],
'text-valign': 'center',
'text-margin-y': -8,
},
},
// Entity nodes - Teal
{
selector: 'node[type="entity"]',
style: {
'shape': 'round-rectangle',
'border-color': '#15a8a8',
'background-color': 'rgba(21, 168, 168, 0.12)',
'width': 95,
'height': 45,
},
},
// Access group nodes - Magenta
{
selector: 'node[type="accessGroup"]',
style: {
'shape': 'ellipse',
'border-color': '#bf1363',
'background-color': 'rgba(191, 19, 99, 0.1)',
'width': 80,
'height': 80,
},
},
// Selected node
{
selector: 'node:selected',
style: {
'border-width': 3,
'background-color': '#ffffff',
'shadow-blur': 15,
'shadow-color': 'rgba(21, 168, 168, 0.4)',
'shadow-opacity': 1,
'shadow-offset-x': 0,
'shadow-offset-y': 4,
},
},
// Highlighted node (connected to selected)
{
selector: 'node.highlighted',
style: {
'border-width': 3,
'opacity': 1,
},
},
// Dimmed node
{
selector: 'node.dimmed',
style: {
'opacity': 0.25,
},
},
// Hidden node
{
selector: 'node.hidden',
style: {
'display': 'none',
},
},
// Change status: Added - keep type color, add solid border + glow
{
selector: 'node[changeStatus="added"]',
style: {
'border-color': '#2a9d8f',
'border-width': 3,
'background-opacity': 0.5,
'shadow-blur': 15,
'shadow-color': 'rgba(42, 157, 143, 0.5)',
'shadow-opacity': 1,
'shadow-offset-x': 0,
'shadow-offset-y': 0,
},
},
// Change status: Removed - faded with dashed border
{
selector: 'node[changeStatus="removed"]',
style: {
'border-color': '#c44536',
'border-width': 2,
'border-style': 'dashed',
'background-opacity': 0.3,
'opacity': 0.6,
},
},
// Change status: Modified - keep type color, add solid border + subtle glow
{
selector: 'node[changeStatus="modified"]',
style: {
'border-color': '#fe5d26',
'border-width': 3,
'background-opacity': 0.5,
'shadow-blur': 12,
'shadow-color': 'rgba(254, 93, 38, 0.4)',
'shadow-opacity': 1,
'shadow-offset-x': 0,
'shadow-offset-y': 0,
},
},
// Base edge style
{
selector: 'edge',
style: {
'width': 1.5,
'line-color': 'rgba(2, 61, 96, 0.2)',
'target-arrow-color': 'rgba(2, 61, 96, 0.3)',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
'arrow-scale': 0.8,
},
},
// Operates-on edge - Teal
{
selector: 'edge[type="operates-on"]',
style: {
'line-color': '#15a8a8',
'target-arrow-color': '#15a8a8',
'width': 2,
},
},
// Requires-access edge - Magenta
{
selector: 'edge[type="requires-access"]',
style: {
'line-color': '#bf1363',
'target-arrow-color': '#bf1363',
'line-style': 'dashed',
'line-dash-pattern': [6, 3],
},
},
// Depends-on edge - Orange
{
selector: 'edge[type="depends-on"]',
style: {
'line-color': '#fe5d26',
'target-arrow-color': '#fe5d26',
'line-style': 'dotted',
'line-dash-pattern': [2, 4],
},
},
// Highlighted edge
{
selector: 'edge.highlighted',
style: {
'width': 3,
'opacity': 1,
},
},
// Dimmed edge
{
selector: 'edge.dimmed',
style: {
'opacity': 0.12,
},
},
// Hidden edge
{
selector: 'edge.hidden',
style: {
'display': 'none',
},
},
];
}
export function applyLayout(cy: any, layoutName: LayoutType, nodeCount: number) {
const layouts: Record = {
cose: {
name: 'cose',
animate: true,
animationDuration: 500,
nodeRepulsion: 8000,
idealEdgeLength: 100,
gravity: 0.25,
padding: 50,
},
circle: {
name: 'circle',
animate: true,
animationDuration: 500,
padding: 50,
},
grid: {
name: 'grid',
animate: true,
animationDuration: 500,
padding: 50,
rows: Math.ceil(Math.sqrt(nodeCount)),
},
};
cy.layout(layouts[layoutName]).run();
}
export async function loadFonts() {
try {
await Promise.all([
document.fonts.load('500 12px "Space Grotesk"'),
document.fonts.load('600 12px "Space Grotesk"'),
document.fonts.load('400 12px "Space Mono"'),
]);
console.log('Fonts loaded:', document.fonts.check('500 12px "Space Grotesk"') ? 'Space Grotesk OK' : 'Space Grotesk FAILED');
} catch (e) {
console.warn('Font loading error:', e);
}
}