nexus-shell 0.2.4 → 0.2.6

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,12 @@ 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
+ | `GraphMiniMap` | Scaled graph overview with a viewport indicator |
136
+ | `NodePalette` | Drag-to-create and keyboard-to-create node types |
137
+ | `useGraphKeyboard` | Spatial keyboard navigation and editing for a graph |
132
138
  | `UserProfile` | Avatar and identity widget |
133
139
 
134
140
  ## Theming
@@ -0,0 +1,110 @@
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
+ /** The element's pixel size. A minimap needs this to draw its indicator. */
16
+ getSize: () => {
17
+ width: number;
18
+ height: number;
19
+ };
20
+ /** Move keyboard focus to the canvas. */
21
+ focus: () => void;
22
+ }
23
+ export interface GraphCanvasProps {
24
+ /**
25
+ * Content rendered in graph space. Position children with `GraphNode`, or
26
+ * with your own absolutely-positioned elements in graph coordinates.
27
+ */
28
+ children?: ReactNode;
29
+ /**
30
+ * Content rendered in screen space, above the graph and unaffected by pan or
31
+ * zoom — toolbars, a minimap, a legend.
32
+ */
33
+ overlay?: ReactNode;
34
+ /**
35
+ * Take control of pan and zoom. Omit it and the canvas manages its own,
36
+ * which is what most callers want: you should not have to own a viewport
37
+ * just to draw a graph.
38
+ */
39
+ viewport?: IViewport;
40
+ /** Starting viewport when uncontrolled. Defaults to origin at 1× zoom. */
41
+ defaultViewport?: IViewport;
42
+ /** Called whenever pan or zoom changes, controlled or not. */
43
+ onViewportChange?: (viewport: IViewport) => void;
44
+ /**
45
+ * Called on mount and whenever the element resizes, with its pixel size.
46
+ *
47
+ * Pass this to a {@link GraphMiniMap} as `canvasSize` — the indicator cannot
48
+ * be drawn without knowing how much of the graph is on screen.
49
+ */
50
+ onSizeChange?: (size: {
51
+ width: number;
52
+ height: number;
53
+ }) => void;
54
+ /**
55
+ * Called with a graph-space point when empty canvas is clicked. Use it to
56
+ * clear selection, or to place a node at the click.
57
+ */
58
+ onCanvasClick?: (point: IPoint, event: React.MouseEvent) => void;
59
+ /** Called with a graph-space point when empty canvas is double-clicked. */
60
+ onCanvasDoubleClick?: (point: IPoint, event: React.MouseEvent) => void;
61
+ /** Called with a graph-space point on right-click of empty canvas. */
62
+ onCanvasContextMenu?: (point: IPoint, event: React.MouseEvent) => void;
63
+ /**
64
+ * Called when something is dropped on the canvas, with the graph-space point
65
+ * under the cursor. Pair with `NodePalette` for drag-to-create.
66
+ */
67
+ onDrop?: (point: IPoint, event: React.DragEvent) => void;
68
+ /** Show the dot grid. Defaults to `true`. */
69
+ grid?: boolean;
70
+ /** Grid spacing in graph units. Defaults to `24`. */
71
+ gridSize?: number;
72
+ /** Smallest allowed zoom. Defaults to `0.1`. */
73
+ minScale?: number;
74
+ /** Largest allowed zoom. Defaults to `4`. */
75
+ maxScale?: number;
76
+ /** Allow panning by dragging empty canvas. Defaults to `true`. */
77
+ pannable?: boolean;
78
+ /** Allow zooming with the wheel or trackpad pinch. Defaults to `true`. */
79
+ zoomable?: boolean;
80
+ /** Accessible label for the canvas region. Defaults to `"Graph canvas"`. */
81
+ 'aria-label'?: string;
82
+ /** Extra classes merged onto the root element. */
83
+ className?: string;
84
+ }
85
+ /**
86
+ * An infinite, pannable, zoomable field to draw a graph on.
87
+ *
88
+ * The canvas owns exactly one thing: the mapping between screen pixels and
89
+ * graph space. It knows nothing about nodes, edges, or what any of it means —
90
+ * children are rendered inside a transformed layer, so anything positioned in
91
+ * graph coordinates lands in the right place.
92
+ *
93
+ * Pan by dragging empty space or with the middle mouse button; zoom with the
94
+ * wheel or a trackpad pinch, always anchored to the cursor. Space-drag pans
95
+ * from anywhere, including over a node.
96
+ *
97
+ * Viewport state is internal by default. Pass `viewport` to take control.
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * <GraphCanvas onDrop={(point) => addNodeAt(point)}>
102
+ * {nodes.map((node) => (
103
+ * <GraphNode key={node.id} node={node} onMove={move}>
104
+ * {node.label}
105
+ * </GraphNode>
106
+ * ))}
107
+ * </GraphCanvas>
108
+ * ```
109
+ */
110
+ 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,83 @@
1
+ import { IGraphNode, IViewport } from '../../lib/graph';
2
+
3
+ export interface GraphMiniMapProps {
4
+ /** The nodes to plot. Only position and size are read. */
5
+ nodes: IGraphNode[];
6
+ /** The canvas's current pan and zoom. */
7
+ viewport: IViewport;
8
+ /**
9
+ * Pixel size of the canvas the minimap mirrors. Get it from `GraphCanvas`'s
10
+ * `onSizeChange`; without it the viewport indicator can't be drawn.
11
+ */
12
+ canvasSize: {
13
+ width: number;
14
+ height: number;
15
+ };
16
+ /**
17
+ * Called with a viewport that recentres the canvas. Omit it and the minimap
18
+ * becomes a read-only overview.
19
+ */
20
+ onViewportChange?: (viewport: IViewport) => void;
21
+ /** Minimap width in pixels. Defaults to `180`. */
22
+ width?: number;
23
+ /** Minimap height in pixels. Defaults to `120`. */
24
+ height?: number;
25
+ /**
26
+ * Fill colour for a node, as a CSS colour or Tailwind class. Receives the
27
+ * node, so you can colour by `kind`. Defaults to the muted foreground.
28
+ */
29
+ nodeColor?: (node: IGraphNode) => string;
30
+ /**
31
+ * Highlight these node ids — a selection, or search results. Drawn in the
32
+ * primary colour above the rest.
33
+ */
34
+ highlightIds?: string[];
35
+ /** Padding around the plotted content, in minimap pixels. Defaults to `8`. */
36
+ padding?: number;
37
+ /** How far an arrow key pans, in graph units. Defaults to `80`. */
38
+ panDistance?: number;
39
+ /** Accessible label. Defaults to `"Graph minimap"`. */
40
+ 'aria-label'?: string;
41
+ /** Extra classes merged onto the root element. */
42
+ className?: string;
43
+ }
44
+ /**
45
+ * A scaled overview of the whole graph, with an indicator showing what the
46
+ * canvas is currently looking at.
47
+ *
48
+ * Click anywhere to jump there, drag to pan continuously, or focus it and use
49
+ * the arrow keys. The zoom level is never changed — a minimap answers "where am
50
+ * I", and changing scale on a click makes that answer harder to trust.
51
+ *
52
+ * The plotted extent is the union of the graph's bounds and the current
53
+ * viewport, so the indicator stays visible even when you have panned far away
54
+ * from every node. Without that, panning into empty space makes the minimap
55
+ * appear broken.
56
+ *
57
+ * Fully controlled: it reads `viewport` and reports where you want to go, but
58
+ * never moves the canvas itself.
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * const [viewport, setViewport] = useState(IDENTITY_VIEWPORT);
63
+ * const [size, setSize] = useState({ width: 0, height: 0 });
64
+ *
65
+ * <GraphCanvas
66
+ * viewport={viewport}
67
+ * onViewportChange={setViewport}
68
+ * onSizeChange={setSize}
69
+ * overlay={
70
+ * <GraphMiniMap
71
+ * nodes={nodes}
72
+ * viewport={viewport}
73
+ * canvasSize={size}
74
+ * onViewportChange={setViewport}
75
+ * className="absolute bottom-3 right-3"
76
+ * />
77
+ * }
78
+ * >
79
+ * …
80
+ * </GraphCanvas>
81
+ * ```
82
+ */
83
+ export declare const GraphMiniMap: ({ nodes, viewport, canvasSize, onViewportChange, width, height, nodeColor, highlightIds, padding, panDistance, "aria-label": ariaLabel, className, }: GraphMiniMapProps) => 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,117 @@
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
+ /**
6
+ * How the palette lays its items out.
7
+ *
8
+ * - `horizontal` — a row, wrapping if it must.
9
+ * - `vertical` — a column.
10
+ * - `auto` — whichever fits the space the palette is given. See
11
+ * {@link NodePalette} for the rule.
12
+ */
13
+ export type PaletteOrientation = 'horizontal' | 'vertical' | 'auto';
14
+ /** The orientation actually rendered, once `auto` has been resolved. */
15
+ export type ResolvedOrientation = Exclude<PaletteOrientation, 'auto'>;
16
+ /** An entry in a {@link NodePalette}. */
17
+ export interface INodePaletteItem {
18
+ /**
19
+ * The `kind` given to the node this creates. Also the payload carried on the
20
+ * drag, so the drop handler knows what was dragged.
21
+ */
22
+ kind: string;
23
+ /** Label shown in the palette. */
24
+ label: string;
25
+ /** Optional icon or swatch rendered before the label. */
26
+ icon?: ReactNode;
27
+ /** One line explaining what the node is, shown as a tooltip. */
28
+ description?: string;
29
+ }
30
+ export interface NodePaletteProps {
31
+ /** The node types on offer. */
32
+ items: INodePaletteItem[];
33
+ /**
34
+ * Called when an item is activated by click or Enter, for creating without a
35
+ * drag. Keyboard users need this — a drag-only palette is unreachable.
36
+ */
37
+ onSelect?: (item: INodePaletteItem) => void;
38
+ /** Called when a drag starts, if you want to show a drop hint. */
39
+ onDragStart?: (item: INodePaletteItem) => void;
40
+ /**
41
+ * Layout axis. Defaults to `"auto"`, which measures the available space and
42
+ * picks whichever axis fits.
43
+ */
44
+ orientation?: PaletteOrientation;
45
+ /**
46
+ * Called when `auto` settles on an axis, and whenever it changes. Useful for
47
+ * adjusting surrounding layout to match.
48
+ */
49
+ onOrientationChange?: (orientation: ResolvedOrientation) => void;
50
+ /**
51
+ * Hide labels and show icons only. Useful for a narrow rail; items keep their
52
+ * accessible name, so nothing is lost to a screen reader.
53
+ */
54
+ iconOnly?: boolean;
55
+ /** Extra classes merged onto the root element. */
56
+ className?: string;
57
+ /** Accessible label for the group. Defaults to `"Node palette"`. */
58
+ 'aria-label'?: string;
59
+ }
60
+ /**
61
+ * A palette of node types that can be dragged onto a {@link GraphCanvas}, or
62
+ * activated with the keyboard.
63
+ *
64
+ * The palette carries only the item's `kind` on the drag; what a node of that
65
+ * kind actually *is* is decided by your drop handler. Pair it with
66
+ * {@link readPaletteDrag} on the canvas.
67
+ *
68
+ * Every item is a real button, so the palette is fully usable without a
69
+ * pointer — `onSelect` fires on click or Enter.
70
+ *
71
+ * ## Orientation
72
+ *
73
+ * `auto` (the default) measures the space the palette's container gives it and
74
+ * picks the axis that fits:
75
+ *
76
+ * 1. a row, if the items fit the available width;
77
+ * 2. otherwise a column, if they fit the available height;
78
+ * 3. otherwise a row that wraps.
79
+ *
80
+ * A container that shrink-wraps its content — an absolutely positioned overlay,
81
+ * say — offers no information about available space, so the palette measures
82
+ * the nearest ancestor that has a size of its own. That is what stops a
83
+ * vertical palette from narrowing its own container and then being unable to
84
+ * discover it has room to go horizontal again.
85
+ *
86
+ * Pass `horizontal` or `vertical` when you already know.
87
+ *
88
+ * @example
89
+ * ```tsx
90
+ * <GraphCanvas
91
+ * onDrop={(point, event) => {
92
+ * const kind = readPaletteDrag(event);
93
+ * if (kind) addNode({ kind, position: point });
94
+ * }}
95
+ * overlay={
96
+ * <NodePalette
97
+ * items={[{ kind: 'task', label: 'Task' }, { kind: 'gate', label: 'Gate' }]}
98
+ * onSelect={(item) => addNodeAtCentre(item.kind)}
99
+ * />
100
+ * }
101
+ * />
102
+ * ```
103
+ */
104
+ export declare const NodePalette: ({ items, onSelect, onDragStart, orientation, onOrientationChange, iconOnly, className, "aria-label": ariaLabel, }: NodePaletteProps) => import("react/jsx-runtime").JSX.Element;
105
+ /**
106
+ * Read the node kind from a drop on the canvas, or `null` if the drag did not
107
+ * come from a {@link NodePalette}.
108
+ *
109
+ * @example
110
+ * ```tsx
111
+ * <GraphCanvas onDrop={(point, event) => {
112
+ * const kind = readPaletteDrag(event);
113
+ * if (kind) createNode(kind, point);
114
+ * }} />
115
+ * ```
116
+ */
117
+ export declare const readPaletteDrag: (event: React.DragEvent) => string | null;
@@ -0,0 +1,17 @@
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 { GraphMiniMap, type GraphMiniMapProps } from './GraphMiniMap';
17
+ export { NodePalette, readPaletteDrag, GRAPH_NODE_MIME, type NodePaletteProps, type INodePaletteItem, type PaletteOrientation, type ResolvedOrientation, } 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, unionRect, viewportRect, centerOn, 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';