nexus-shell 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -129,6 +129,11 @@ shell's own frame and keep every registration working.
129
129
  | `TerminalPane` | Bottom-docked terminal log and input |
130
130
  | `ThemeSwitcher` | Segmented theme control |
131
131
  | `TreeWidget` | Virtualised tree for any hierarchy, with drag-to-move and a data-driven menu |
132
+ | `GraphCanvas` | Infinite pannable, zoomable field for node-and-edge editing |
133
+ | `GraphNode` | Positioned, focusable node with edge ports |
134
+ | `GraphEdge` | Directed edge with bezier / smoothstep / straight routing |
135
+ | `NodePalette` | Drag-to-create and keyboard-to-create node types |
136
+ | `useGraphKeyboard` | Spatial keyboard navigation and editing for a graph |
132
137
  | `UserProfile` | Avatar and identity widget |
133
138
 
134
139
  ## Theming
@@ -0,0 +1,95 @@
1
+ import { default as React, ReactNode } from 'react';
2
+ import { IGraphNode, IPoint, IViewport } from '../../lib/graph';
3
+
4
+ export interface GraphCanvasHandle {
5
+ /** Current pan and zoom. */
6
+ getViewport: () => IViewport;
7
+ /** Jump to a viewport. */
8
+ setViewport: (viewport: IViewport) => void;
9
+ /** Frame the given nodes, or clear the viewport when the list is empty. */
10
+ fitTo: (nodes: IGraphNode[], padding?: number) => void;
11
+ /** Multiply the zoom about the centre of the element. */
12
+ zoomBy: (factor: number) => void;
13
+ /** Convert a client (page) point into graph space. */
14
+ clientToGraph: (client: IPoint) => IPoint;
15
+ /** Move keyboard focus to the canvas. */
16
+ focus: () => void;
17
+ }
18
+ export interface GraphCanvasProps {
19
+ /**
20
+ * Content rendered in graph space. Position children with `GraphNode`, or
21
+ * with your own absolutely-positioned elements in graph coordinates.
22
+ */
23
+ children?: ReactNode;
24
+ /**
25
+ * Content rendered in screen space, above the graph and unaffected by pan or
26
+ * zoom — toolbars, a minimap, a legend.
27
+ */
28
+ overlay?: ReactNode;
29
+ /**
30
+ * Take control of pan and zoom. Omit it and the canvas manages its own,
31
+ * which is what most callers want: you should not have to own a viewport
32
+ * just to draw a graph.
33
+ */
34
+ viewport?: IViewport;
35
+ /** Starting viewport when uncontrolled. Defaults to origin at 1× zoom. */
36
+ defaultViewport?: IViewport;
37
+ /** Called whenever pan or zoom changes, controlled or not. */
38
+ onViewportChange?: (viewport: IViewport) => void;
39
+ /**
40
+ * Called with a graph-space point when empty canvas is clicked. Use it to
41
+ * clear selection, or to place a node at the click.
42
+ */
43
+ onCanvasClick?: (point: IPoint, event: React.MouseEvent) => void;
44
+ /** Called with a graph-space point when empty canvas is double-clicked. */
45
+ onCanvasDoubleClick?: (point: IPoint, event: React.MouseEvent) => void;
46
+ /** Called with a graph-space point on right-click of empty canvas. */
47
+ onCanvasContextMenu?: (point: IPoint, event: React.MouseEvent) => void;
48
+ /**
49
+ * Called when something is dropped on the canvas, with the graph-space point
50
+ * under the cursor. Pair with `NodePalette` for drag-to-create.
51
+ */
52
+ onDrop?: (point: IPoint, event: React.DragEvent) => void;
53
+ /** Show the dot grid. Defaults to `true`. */
54
+ grid?: boolean;
55
+ /** Grid spacing in graph units. Defaults to `24`. */
56
+ gridSize?: number;
57
+ /** Smallest allowed zoom. Defaults to `0.1`. */
58
+ minScale?: number;
59
+ /** Largest allowed zoom. Defaults to `4`. */
60
+ maxScale?: number;
61
+ /** Allow panning by dragging empty canvas. Defaults to `true`. */
62
+ pannable?: boolean;
63
+ /** Allow zooming with the wheel or trackpad pinch. Defaults to `true`. */
64
+ zoomable?: boolean;
65
+ /** Accessible label for the canvas region. Defaults to `"Graph canvas"`. */
66
+ 'aria-label'?: string;
67
+ /** Extra classes merged onto the root element. */
68
+ className?: string;
69
+ }
70
+ /**
71
+ * An infinite, pannable, zoomable field to draw a graph on.
72
+ *
73
+ * The canvas owns exactly one thing: the mapping between screen pixels and
74
+ * graph space. It knows nothing about nodes, edges, or what any of it means —
75
+ * children are rendered inside a transformed layer, so anything positioned in
76
+ * graph coordinates lands in the right place.
77
+ *
78
+ * Pan by dragging empty space or with the middle mouse button; zoom with the
79
+ * wheel or a trackpad pinch, always anchored to the cursor. Space-drag pans
80
+ * from anywhere, including over a node.
81
+ *
82
+ * Viewport state is internal by default. Pass `viewport` to take control.
83
+ *
84
+ * @example
85
+ * ```tsx
86
+ * <GraphCanvas onDrop={(point) => addNodeAt(point)}>
87
+ * {nodes.map((node) => (
88
+ * <GraphNode key={node.id} node={node} onMove={move}>
89
+ * {node.label}
90
+ * </GraphNode>
91
+ * ))}
92
+ * </GraphCanvas>
93
+ * ```
94
+ */
95
+ export declare const GraphCanvas: React.ForwardRefExoticComponent<GraphCanvasProps & React.RefAttributes<GraphCanvasHandle>>;
@@ -0,0 +1,71 @@
1
+ import { default as React } from 'react';
2
+ import { GraphPort, IGraphEdge, IGraphNode, IPoint } from '../../lib/graph';
3
+
4
+ /**
5
+ * How an edge gets from one node to the other.
6
+ *
7
+ * - `bezier` — a smooth curve. The default; reads well for dense graphs.
8
+ * - `smoothstep` — right angles with rounded corners, for flowcharts.
9
+ * - `straight` — a direct line.
10
+ */
11
+ export type EdgeRouting = 'bezier' | 'smoothstep' | 'straight';
12
+ export interface GraphEdgeProps {
13
+ /** The edge to render. */
14
+ edge: IGraphEdge;
15
+ /** The node the edge leaves. */
16
+ source: IGraphNode;
17
+ /** The node the edge enters. */
18
+ target: IGraphNode;
19
+ /** Path shape. Defaults to `"bezier"`. */
20
+ routing?: EdgeRouting;
21
+ /** Draw as selected. */
22
+ selected?: boolean;
23
+ /** Draw an arrowhead at the target end. Defaults to `true`. */
24
+ arrow?: boolean;
25
+ /** Called on click. The wide invisible hit area makes this practical. */
26
+ onSelect?: (id: string, event: React.MouseEvent) => void;
27
+ /** Called on right-click, with the client point for a context menu. */
28
+ onContextMenu?: (id: string, point: IPoint, event: React.MouseEvent) => void;
29
+ /** Extra classes merged onto the visible path. */
30
+ className?: string;
31
+ }
32
+ /** The SVG path for a pair of points and the sides they leave from. */
33
+ export declare const edgePath: (from: IPoint, to: IPoint, fromPort: GraphPort, routing: EdgeRouting) => string;
34
+ /**
35
+ * A directed edge between two nodes, rendered as an SVG path.
36
+ *
37
+ * Which sides the edge leaves and enters is worked out from the nodes'
38
+ * relative positions, so a node needs no handle configuration to be
39
+ * connectable — move a node and the attachment points follow.
40
+ *
41
+ * Render inside {@link GraphEdgeLayer}, which provides the SVG surface and the
42
+ * arrowhead marker.
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * <GraphEdgeLayer>
47
+ * {edges.map((edge) => (
48
+ * <GraphEdge
49
+ * key={edge.id}
50
+ * edge={edge}
51
+ * source={byId[edge.source]}
52
+ * target={byId[edge.target]}
53
+ * onSelect={setSelectedEdge}
54
+ * />
55
+ * ))}
56
+ * </GraphEdgeLayer>
57
+ * ```
58
+ */
59
+ export declare const GraphEdge: ({ edge, source, target, routing, selected, arrow, onSelect, onContextMenu, className, }: GraphEdgeProps) => import("react/jsx-runtime").JSX.Element;
60
+ export interface GraphEdgeLayerProps {
61
+ children?: React.ReactNode;
62
+ className?: string;
63
+ }
64
+ /**
65
+ * The SVG surface edges are drawn on.
66
+ *
67
+ * Sits beneath the nodes inside a {@link GraphCanvas}, spanning an effectively
68
+ * unbounded area so edges remain visible wherever the graph is panned. It also
69
+ * defines the shared arrowhead marker.
70
+ */
71
+ export declare const GraphEdgeLayer: ({ children, className }: GraphEdgeLayerProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,68 @@
1
+ import { default as React, ReactNode } from 'react';
2
+ import { GraphPort, IGraphNode, IPoint } from '../../lib/graph';
3
+
4
+ export interface GraphNodeProps {
5
+ /** The node to render. Position and size come from here. */
6
+ node: IGraphNode;
7
+ /** Node body. Anything — a label, a form, a chart. */
8
+ children?: ReactNode;
9
+ /**
10
+ * Called with the new top-left position while dragging, and again on
11
+ * release. The component never mutates the node — you apply the move, which
12
+ * is what lets you snap, constrain or reject it.
13
+ */
14
+ onMove?: (id: string, position: IPoint) => void;
15
+ /** Called when the drag ends, if the node actually moved. */
16
+ onMoveEnd?: (id: string, position: IPoint) => void;
17
+ /** Called on click. */
18
+ onSelect?: (id: string, event: React.MouseEvent) => void;
19
+ /** Called on double-click. Conventionally enters edit mode. */
20
+ onActivate?: (id: string) => void;
21
+ /** Called on right-click, with the client point for a context menu. */
22
+ onContextMenu?: (id: string, point: IPoint, event: React.MouseEvent) => void;
23
+ /**
24
+ * Called when one of the node's ports is dragged, to begin a connection.
25
+ * Providing it is what makes the ports appear.
26
+ */
27
+ onConnectStart?: (id: string, port: GraphPort) => void;
28
+ /**
29
+ * Called when a pointer is released over this node, with its id — the target
30
+ * end of a connection started elsewhere.
31
+ */
32
+ onConnectEnd?: (targetId: string) => void;
33
+ /** Draw as selected. */
34
+ selected?: boolean;
35
+ /**
36
+ * Draw as focused — the keyboard cursor. Distinct from `selected`: focus is
37
+ * where the next keystroke lands, selection is what an action applies to.
38
+ */
39
+ focused?: boolean;
40
+ /** Show connection ports on hover and while focused. Defaults to `true`. */
41
+ ports?: boolean;
42
+ /** Allow dragging. Defaults to `true`. */
43
+ draggable?: boolean;
44
+ /** Extra classes merged onto the node element. */
45
+ className?: string;
46
+ }
47
+ /**
48
+ * A positioned, focusable node on a {@link GraphCanvas}.
49
+ *
50
+ * Provides placement, dragging, selection and focus affordances, and the four
51
+ * edge ports. What the node *contains* is entirely yours — pass any children.
52
+ *
53
+ * Fully controlled: dragging reports positions through `onMove` but never moves
54
+ * the node itself, so snapping, constraints and undo stay in your hands.
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * <GraphNode
59
+ * node={node}
60
+ * focused={node.id === focusedId}
61
+ * onMove={(id, position) => setNodes(move(nodes, id, position))}
62
+ * onActivate={(id) => setEditing(id)}
63
+ * >
64
+ * <p className="text-xs font-medium">{node.data.label}</p>
65
+ * </GraphNode>
66
+ * ```
67
+ */
68
+ export declare const GraphNode: ({ node, children, onMove, onMoveEnd, onSelect, onActivate, onContextMenu, onConnectStart, onConnectEnd, selected, focused, ports, draggable, className, }: GraphNodeProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,76 @@
1
+ import { default as React, ReactNode } from 'react';
2
+
3
+ /** The MIME type palette entries put their kind on during a drag. */
4
+ export declare const GRAPH_NODE_MIME = "application/x-nexus-graph-node";
5
+ /** An entry in a {@link NodePalette}. */
6
+ export interface INodePaletteItem {
7
+ /**
8
+ * The `kind` given to the node this creates. Also the payload carried on the
9
+ * drag, so the drop handler knows what was dragged.
10
+ */
11
+ kind: string;
12
+ /** Label shown in the palette. */
13
+ label: string;
14
+ /** Optional icon or swatch rendered before the label. */
15
+ icon?: ReactNode;
16
+ /** One line explaining what the node is, shown as a tooltip. */
17
+ description?: string;
18
+ }
19
+ export interface NodePaletteProps {
20
+ /** The node types on offer. */
21
+ items: INodePaletteItem[];
22
+ /**
23
+ * Called when an item is activated by click or Enter, for creating without a
24
+ * drag. Keyboard users need this — a drag-only palette is unreachable.
25
+ */
26
+ onSelect?: (item: INodePaletteItem) => void;
27
+ /** Called when a drag starts, if you want to show a drop hint. */
28
+ onDragStart?: (item: INodePaletteItem) => void;
29
+ /** Stack vertically instead of wrapping horizontally. Defaults to `false`. */
30
+ vertical?: boolean;
31
+ /** Extra classes merged onto the root element. */
32
+ className?: string;
33
+ /** Accessible label for the group. Defaults to `"Node palette"`. */
34
+ 'aria-label'?: string;
35
+ }
36
+ /**
37
+ * A palette of node types that can be dragged onto a {@link GraphCanvas}, or
38
+ * activated with the keyboard.
39
+ *
40
+ * The palette carries only the item's `kind` on the drag; what a node of that
41
+ * kind actually *is* is decided by your drop handler. Pair it with
42
+ * {@link readPaletteDrag} on the canvas.
43
+ *
44
+ * Every item is a real button, so the palette is fully usable without a
45
+ * pointer — `onSelect` fires on click or Enter.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <GraphCanvas
50
+ * onDrop={(point, event) => {
51
+ * const kind = readPaletteDrag(event);
52
+ * if (kind) addNode({ kind, position: point });
53
+ * }}
54
+ * overlay={
55
+ * <NodePalette
56
+ * items={[{ kind: 'task', label: 'Task' }, { kind: 'gate', label: 'Gate' }]}
57
+ * onSelect={(item) => addNodeAtCentre(item.kind)}
58
+ * />
59
+ * }
60
+ * />
61
+ * ```
62
+ */
63
+ export declare const NodePalette: ({ items, onSelect, onDragStart, vertical, className, "aria-label": ariaLabel, }: NodePaletteProps) => import("react/jsx-runtime").JSX.Element;
64
+ /**
65
+ * Read the node kind from a drop on the canvas, or `null` if the drag did not
66
+ * come from a {@link NodePalette}.
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * <GraphCanvas onDrop={(point, event) => {
71
+ * const kind = readPaletteDrag(event);
72
+ * if (kind) createNode(kind, point);
73
+ * }} />
74
+ * ```
75
+ */
76
+ export declare const readPaletteDrag: (event: React.DragEvent) => string | null;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The graph layer.
3
+ *
4
+ * A decomposed set of primitives for building node-and-edge editors, in the
5
+ * same spirit as the rest of the library: the canvas knows about coordinates,
6
+ * the node knows about placement and focus, the edge knows about geometry, and
7
+ * the keyboard hook knows about traversal. None of them know what a node
8
+ * *means* — that stays in your application.
9
+ *
10
+ * Compose them yourself, or reach for whichever piece you need on its own.
11
+ */
12
+ export { GraphCanvas, type GraphCanvasProps, type GraphCanvasHandle } from './GraphCanvas';
13
+ export { GraphNode, type GraphNodeProps } from './GraphNode';
14
+ export { GraphEdge, GraphEdgeLayer, edgePath, type GraphEdgeProps, type GraphEdgeLayerProps, type EdgeRouting, } from './GraphEdge';
15
+ export { useGraphKeyboard, DEFAULT_GRAPH_KEYS, type UseGraphKeyboardOptions, type UseGraphKeyboardResult, type GraphKeyboardActions, } from './useGraphKeyboard';
16
+ export { NodePalette, readPaletteDrag, GRAPH_NODE_MIME, type NodePaletteProps, type INodePaletteItem, } from './NodePalette';
@@ -0,0 +1,115 @@
1
+ import { Direction, IGraphEdge, IGraphNode, IPoint } from '../../lib/graph';
2
+
3
+ /** The default keyboard map. Every binding is overridable. */
4
+ export declare const DEFAULT_GRAPH_KEYS: {
5
+ /** Move focus to the nearest node in a direction. */
6
+ readonly moveFocus: readonly ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"];
7
+ /** Create a connected node in a direction. */
8
+ readonly createConnected: "Tab";
9
+ /** Edit the focused node. */
10
+ readonly edit: "Enter";
11
+ /** Leave edit mode, cancel a pending connection, or clear focus. */
12
+ readonly cancel: "Escape";
13
+ /** Delete the focused node. */
14
+ readonly delete: readonly ["Delete", "Backspace"];
15
+ /** Begin connecting from the focused node. */
16
+ readonly connect: "c";
17
+ /** Nudge the focused node. Held with the arrow keys. */
18
+ readonly nudgeModifier: "shiftKey";
19
+ };
20
+ export interface GraphKeyboardActions {
21
+ /**
22
+ * Create a node at a position, returning its id so focus can follow it. The
23
+ * library never invents node content — you decide what a new node is.
24
+ */
25
+ onCreateNode?: (position: IPoint, context: {
26
+ from?: string;
27
+ direction?: Direction;
28
+ }) => string | undefined;
29
+ /** Connect two nodes. */
30
+ onConnect?: (sourceId: string, targetId: string) => void;
31
+ /** Delete a node and its edges. */
32
+ onDeleteNode?: (id: string) => void;
33
+ /** Move a node, used by nudging. */
34
+ onMoveNode?: (id: string, position: IPoint) => void;
35
+ /** Enter edit mode for a node. */
36
+ onEditNode?: (id: string) => void;
37
+ }
38
+ export interface UseGraphKeyboardOptions extends GraphKeyboardActions {
39
+ /** The current nodes. Traversal and placement are computed from these. */
40
+ nodes: IGraphNode[];
41
+ /** The current edges. Used to reason about existing connections. */
42
+ edges: IGraphEdge[];
43
+ /** Take control of which node has keyboard focus. */
44
+ focusedId?: string | null;
45
+ /** Starting focus when uncontrolled. */
46
+ defaultFocusedId?: string | null;
47
+ /** Called whenever focus moves. */
48
+ onFocusChange?: (id: string | null) => void;
49
+ /**
50
+ * Element the listener attaches to. Bindings only fire while focus is inside
51
+ * it, so several graphs can coexist on one page.
52
+ */
53
+ targetRef: React.RefObject<HTMLElement | null>;
54
+ /** How far a nudge moves a node, in graph units. Defaults to `16`. */
55
+ nudgeDistance?: number;
56
+ /** Turn the whole thing off. */
57
+ disabled?: boolean;
58
+ }
59
+ export interface UseGraphKeyboardResult {
60
+ /** The node the next keystroke applies to. */
61
+ focusedId: string | null;
62
+ setFocusedId: (id: string | null) => void;
63
+ /** Node currently being edited, if any. */
64
+ editingId: string | null;
65
+ setEditingId: (id: string | null) => void;
66
+ /** Source of a connection awaiting a target, if any. */
67
+ connectingFrom: string | null;
68
+ setConnectingFrom: (id: string | null) => void;
69
+ /** Move focus as an arrow key would. Exposed for toolbar buttons. */
70
+ moveFocus: (direction: Direction) => void;
71
+ /** Create a connected node as Tab would. */
72
+ createConnected: (direction?: Direction) => void;
73
+ }
74
+ /**
75
+ * Keyboard-driven graph navigation and editing.
76
+ *
77
+ * Headless: it owns the focus, editing and connection cursors and reports what
78
+ * should happen, but renders nothing and mutates nothing. That means it works
79
+ * with `GraphNode` or with your own node rendering, and every mutation stays
80
+ * subject to your validation and undo.
81
+ *
82
+ * The default map, all overridable:
83
+ *
84
+ * | Key | Action |
85
+ * | :-- | :-- |
86
+ * | Arrows | Move focus to the nearest node in that direction |
87
+ * | Shift + Arrows | Nudge the focused node |
88
+ * | Tab / Shift+Tab | Create a node connected to the focused one |
89
+ * | Enter | Edit the focused node |
90
+ * | `c` then a node | Connect from the focused node |
91
+ * | Delete / Backspace | Delete the focused node |
92
+ * | Escape | Leave edit mode, cancel a connection, or clear focus |
93
+ *
94
+ * Arrow traversal is spatial rather than by insertion order — it picks the
95
+ * nearest node in the direction travelled, penalising lateral offset, which is
96
+ * what makes a hand-arranged diagram navigable without a mouse.
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * const canvasRef = useRef<HTMLDivElement>(null);
101
+ *
102
+ * const keyboard = useGraphKeyboard({
103
+ * nodes, edges, targetRef: canvasRef,
104
+ * onCreateNode: (position) => {
105
+ * const id = nextId('node', nodes);
106
+ * setNodes([...nodes, { id, position, data: { label: 'New' } }]);
107
+ * return id;
108
+ * },
109
+ * onConnect: (source, target) =>
110
+ * setEdges([...edges, { id: nextId('edge', edges), source, target }]),
111
+ * onDeleteNode: (id) => { … },
112
+ * });
113
+ * ```
114
+ */
115
+ export declare const useGraphKeyboard: ({ nodes, edges, focusedId: controlledFocusedId, defaultFocusedId, onFocusChange, targetRef, nudgeDistance, disabled, onCreateNode, onConnect, onDeleteNode, onMoveNode, onEditNode, }: UseGraphKeyboardOptions) => UseGraphKeyboardResult;
package/dist/index.d.ts CHANGED
@@ -56,5 +56,7 @@ export { TerminalPane, type TerminalPaneProps } from './components/widgets/Termi
56
56
  export { ThemeSwitcher, type ThemeSwitcherProps, type IThemeOption, DEFAULT_THEME_OPTIONS } from './components/widgets/ThemeSwitcher';
57
57
  export { TreeWidget, type TreeWidgetProps, type ITreeNode, type ITreeAction, type ITreeContext } from './components/widgets/TreeWidget';
58
58
  export { UserProfile, type UserProfileProps, type IUserProfile, type IUserProfileAction } from './components/widgets/UserProfile';
59
+ export * from './components/graph';
60
+ export { DEFAULT_NODE_SIZE, IDENTITY_VIEWPORT, clampScale, edgesOf, findNeighbour, fitViewport, graphBounds, isConnected, nextId, nodeRect, placeRelativeTo, portPoint, rectCenter, removeNode, resolvePorts, toGraphSpace, toScreenSpace, zoomAt, type Direction, type GraphPort, type IGraphEdge, type IGraphNode, type IPoint, type IRect, type IViewport, } from './lib/graph';
59
61
  export { cn } from './lib/cn';
60
62
  export { BUNDLED_THEMES, BUNDLED_THEME_CLASSES, themeClass, type IBundledTheme, type BundledThemeId, } from './lib/themes';
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The graph domain model and its geometry.
3
+ *
4
+ * Kept deliberately small and domain-neutral: the library knows about
5
+ * positioned nodes, edges between them, and a viewport. What a node *means* —
6
+ * an argument, a task, a service, a scene object — belongs to the application,
7
+ * exactly as with {@link ITreeNode}.
8
+ *
9
+ * This lives in `lib/` rather than `core/services/` so the presentational graph
10
+ * components can use it; they are barred from importing stores.
11
+ */
12
+ /** A point in graph space. Graph space is unbounded in every direction. */
13
+ export interface IPoint {
14
+ x: number;
15
+ y: number;
16
+ }
17
+ /** A rectangle in graph space, anchored at its top-left corner. */
18
+ export interface IRect extends IPoint {
19
+ width: number;
20
+ height: number;
21
+ }
22
+ /**
23
+ * The four sides a node exposes for edge attachment.
24
+ *
25
+ * Ports are sides rather than discrete handles: an edge picks whichever side
26
+ * faces its counterpart, which keeps the model small and means a node needs no
27
+ * per-handle configuration to be connectable.
28
+ */
29
+ export type GraphPort = 'top' | 'right' | 'bottom' | 'left';
30
+ /** A node in the graph. */
31
+ export interface IGraphNode {
32
+ /** Stable identifier, unique across the graph. */
33
+ id: string;
34
+ /** Top-left corner in graph space. */
35
+ position: IPoint;
36
+ /** Rendered size. Defaults to {@link DEFAULT_NODE_SIZE} when omitted. */
37
+ size?: {
38
+ width: number;
39
+ height: number;
40
+ };
41
+ /**
42
+ * Your own classification, e.g. `"question"`, `"service"`, `"task"`. Never
43
+ * interpreted by the library — it exists so you can render and validate by
44
+ * type, and so palette entries and edge rules can target it.
45
+ */
46
+ kind?: string;
47
+ /** Free-form payload. The library passes it through untouched. */
48
+ data?: unknown;
49
+ }
50
+ /** A directed edge between two nodes. */
51
+ export interface IGraphEdge {
52
+ /** Stable identifier, unique across the graph. */
53
+ id: string;
54
+ /** Id of the node the edge leaves. */
55
+ source: string;
56
+ /** Id of the node the edge enters. */
57
+ target: string;
58
+ /** Your own classification, e.g. `"supports"`, `"depends-on"`. */
59
+ kind?: string;
60
+ /** Text rendered at the edge's midpoint. */
61
+ label?: string;
62
+ /** Free-form payload. */
63
+ data?: unknown;
64
+ }
65
+ /** Pan and zoom. `scale` multiplies graph units to screen pixels. */
66
+ export interface IViewport {
67
+ x: number;
68
+ y: number;
69
+ scale: number;
70
+ }
71
+ export declare const DEFAULT_NODE_SIZE: {
72
+ readonly width: 180;
73
+ readonly height: 72;
74
+ };
75
+ export declare const IDENTITY_VIEWPORT: IViewport;
76
+ /** The rectangle a node occupies, filling in the default size. */
77
+ export declare const nodeRect: (node: IGraphNode) => IRect;
78
+ export declare const rectCenter: (rect: IRect) => IPoint;
79
+ /** Convert a screen-space point (relative to the canvas element) to graph space. */
80
+ export declare const toGraphSpace: (point: IPoint, viewport: IViewport) => IPoint;
81
+ /** Convert a graph-space point to screen space (relative to the canvas element). */
82
+ export declare const toScreenSpace: (point: IPoint, viewport: IViewport) => IPoint;
83
+ /**
84
+ * Zoom about a fixed screen point, so the graph position under the cursor stays
85
+ * under the cursor. Anything else feels wrong to use.
86
+ */
87
+ export declare const zoomAt: (viewport: IViewport, screenPoint: IPoint, nextScale: number) => IViewport;
88
+ export declare const clampScale: (scale: number, min?: number, max?: number) => number;
89
+ /** The point on a rectangle's edge for a given side. */
90
+ export declare const portPoint: (rect: IRect, port: GraphPort) => IPoint;
91
+ /**
92
+ * Choose which sides an edge should leave and enter, based on which direction
93
+ * dominates. Picking automatically is what lets a node be connectable without
94
+ * declaring handles.
95
+ */
96
+ export declare const resolvePorts: (from: IRect, to: IRect) => {
97
+ source: GraphPort;
98
+ target: GraphPort;
99
+ };
100
+ /**
101
+ * The bounding box of a set of nodes, or `null` when there are none.
102
+ * Used to fit the viewport to the graph.
103
+ */
104
+ export declare const graphBounds: (nodes: IGraphNode[]) => IRect | null;
105
+ /** A viewport that fits `nodes` inside a element of the given size. */
106
+ export declare const fitViewport: (nodes: IGraphNode[], size: {
107
+ width: number;
108
+ height: number;
109
+ }, padding?: number) => IViewport;
110
+ export type Direction = 'up' | 'down' | 'left' | 'right';
111
+ /**
112
+ * The nearest node in a direction, for arrow-key traversal.
113
+ *
114
+ * Scores candidates by distance along the travel axis, penalised by how far
115
+ * they stray off it. Pure distance would jump to a node that is barely in the
116
+ * right direction but far to the side; requiring perfect alignment would make
117
+ * most of the graph unreachable. The 2× penalty on lateral offset is the
118
+ * compromise, and it matches what feels right when navigating a hand-arranged
119
+ * diagram.
120
+ *
121
+ * Returns `null` when nothing lies in that direction.
122
+ */
123
+ export declare const findNeighbour: (nodes: IGraphNode[], fromId: string, direction: Direction) => IGraphNode | null;
124
+ /**
125
+ * A free position for a new node placed relative to an existing one, nudged
126
+ * along `direction` and then away from anything it would overlap.
127
+ *
128
+ * Used when creating a node from the keyboard, where there is no cursor to say
129
+ * where it should go.
130
+ */
131
+ export declare const placeRelativeTo: (nodes: IGraphNode[], fromId: string, direction: Direction, gap?: number) => IPoint;
132
+ /** Edges touching a node, in either direction. */
133
+ export declare const edgesOf: (edges: IGraphEdge[], nodeId: string) => IGraphEdge[];
134
+ /** Whether an edge already joins two nodes, in either direction. */
135
+ export declare const isConnected: (edges: IGraphEdge[], a: string, b: string) => boolean;
136
+ /** Remove a node and every edge attached to it. */
137
+ export declare const removeNode: (nodes: IGraphNode[], edges: IGraphEdge[], nodeId: string) => {
138
+ nodes: IGraphNode[];
139
+ edges: IGraphEdge[];
140
+ };
141
+ /** Generate an id that is unique within a collection. */
142
+ export declare const nextId: (prefix: string, existing: {
143
+ id: string;
144
+ }[]) => string;