Cytoscape adapter
@ship-it-ui/cytoscape is a peer-dep adapter — three layered exports that
let a Cytoscape-driven graph share the design system's color tokens, dark /
light themes, and entity-type vocabulary without duplicating the
getComputedStyle + MutationObserver dance in every app.
Install#
pnpm add cytoscape @ship-it-ui/cytoscapeCytoscape itself is a peer dependency — the adapter does not bundle a version or pin to any specific extensions.
End-to-end example#
A real explorer — 12 nodes across six entity types, 14 edges (solid / on-path / dimmed), force-directed layout, click to inspect. Pan with drag, zoom with scroll. The visual vocabulary (glyphs, entity colors, edge states) flows entirely from the registry — no consumer-side stylesheet overrides.
Stylesheet builder#
buildShipItStylesheet(options?) returns a Cytoscape StylesheetJson array
built from the live token palette. Pass a pre-resolved palette for SSR or
deterministic tests; append app-specific selectors via extra.
import cytoscape from 'cytoscape';
import { buildShipItStylesheet } from '@ship-it-ui/cytoscape';
const cy = cytoscape({
container: document.getElementById('graph'),
elements: [...],
style: buildShipItStylesheet(),
});The exported GRAPH_CANVAS_CLASS constants are the class names the
stylesheet recognizes (graph-canvas:path, graph-canvas:dim). Toggle them
on nodes / edges to drive the on-path and dimmed visuals.
Adapter edges vs. <GraphEdge>#
The two surfaces are parallel, not 1:1. <GraphEdge> is an SVG primitive
with four visual variants (solid / dashed / highlighted / dim); the
cytoscape adapter exposes state classes (graph-canvas:path,
graph-canvas:dim) tuned for in-canvas readability. If you need
SVG-styled annotations drawn over a cytoscape graph (animated highlights,
preview paths), layer <PathOverlay> on top rather than trying to express
those variants through the stylesheet.
Extending the stylesheet#
options.extra appends raw blocks after the built-in selectors — that's the
intended path for app-specific looks like dashed relationships, custom node
chrome, or extra :selected states. Toggle the class on the cytoscape
element to apply it.
import cytoscape from 'cytoscape';
import { buildShipItStylesheet } from '@ship-it-ui/cytoscape';
const cy = cytoscape({
container: document.getElementById('graph'),
elements: [...],
style: buildShipItStylesheet({
extra: [
{
selector: 'edge.relationship-dashed',
style: {
'line-style': 'dashed',
'line-dash-pattern': [4, 3],
},
},
],
}),
});
cy.getElementById('edge-1').addClass('relationship-dashed');Theme-aware re-resolver hook#
useShipItStylesheet(cyRef) re-applies the stylesheet whenever
<html data-theme> flips. It wires a MutationObserver against the document
root (skippable via observe: false) and returns a refresh() callback for
manual triggers — useful after a --color-accent hue-knob change.
const cyRef = useRef<cytoscape.Core | null>(null);
const { refresh } = useShipItStylesheet(cyRef);<GraphCanvas> wrapper#
<GraphCanvas engine={cytoscape} …/> owns the Cytoscape lifecycle, the
theme ↔ stylesheet sync, and a thin selection API. Pass the imported
cytoscape default export as engine — the wrapper never bundles
Cytoscape itself.
import cytoscape from 'cytoscape';
import { GraphCanvas } from '@ship-it-ui/cytoscape';
import { GraphInspector } from '@ship-it-ui/shipit';
<GraphCanvas
engine={cytoscape}
elements={elements}
layout={{ name: 'cose' }}
onSelect={(node) => setSelected(node.id())}
inspector={selected && <GraphInspector type={…} title={…} />}
/>;Custom entity types#
Node colors flow through the entity-type registry exported by
@ship-it-ui/shipit. Register your own types once and they appear in the
graph without forking the stylesheet:
import { registerEntityTypes } from '@ship-it-ui/shipit';
registerEntityTypes({
repository: {
glyph: '◆',
label: 'Repository',
toneClass: 'text-accent',
toneBg: 'bg-accent-dim',
colorVar: 'var(--color-accent)',
badgeVariant: 'accent',
},
});Once registered, nodes with data.entityType = 'repository' pick up the
accent ring automatically — buildShipItStylesheet walks the registry on
every call and emits one node[entityType = "…"] selector per registered
type, so the ring color is driven entirely by the type's colorVar. Register
before the first <GraphCanvas> mount (or call the imperative handle's
refreshStyles() afterward) so the stylesheet picks up the new entries.