@luciole-sh/flow-graph 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +671 -2
- package/dist/Flow.d.ts +41 -0
- package/dist/Flow.d.ts.map +1 -0
- package/dist/Flow.js +489 -0
- package/dist/Flow.js.map +1 -0
- package/dist/components.d.ts +28 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +100 -0
- package/dist/components.js.map +1 -0
- package/dist/frame.d.ts +68 -0
- package/dist/frame.d.ts.map +1 -0
- package/dist/frame.js +136 -0
- package/dist/frame.js.map +1 -0
- package/dist/geometry.d.ts +60 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +205 -0
- package/dist/geometry.js.map +1 -0
- package/dist/hooks.d.ts +56 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +65 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/keymap.d.ts +11 -0
- package/dist/keymap.d.ts.map +1 -0
- package/dist/keymap.js +42 -0
- package/dist/keymap.js.map +1 -0
- package/dist/navigation.d.ts +32 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +68 -0
- package/dist/navigation.js.map +1 -0
- package/dist/nodes.d.ts +18 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +46 -0
- package/dist/nodes.js.map +1 -0
- package/dist/raster.d.ts +112 -0
- package/dist/raster.d.ts.map +1 -0
- package/dist/raster.js +370 -0
- package/dist/raster.js.map +1 -0
- package/dist/store.d.ts +104 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +186 -0
- package/dist/store.js.map +1 -0
- package/dist/theme.d.ts +25 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +39 -0
- package/dist/theme.js.map +1 -0
- package/dist/types.d.ts +140 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vendor/xyflow/LICENSE +21 -0
- package/dist/vendor/xyflow/bezier.d.ts +26 -0
- package/dist/vendor/xyflow/bezier.d.ts.map +1 -0
- package/dist/vendor/xyflow/bezier.js +47 -0
- package/dist/vendor/xyflow/bezier.js.map +1 -0
- package/dist/vendor/xyflow/changes.d.ts +9 -0
- package/dist/vendor/xyflow/changes.d.ts.map +1 -0
- package/dist/vendor/xyflow/changes.js +133 -0
- package/dist/vendor/xyflow/changes.js.map +1 -0
- package/dist/vendor/xyflow/smoothstep.d.ts +17 -0
- package/dist/vendor/xyflow/smoothstep.d.ts.map +1 -0
- package/dist/vendor/xyflow/smoothstep.js +166 -0
- package/dist/vendor/xyflow/smoothstep.js.map +1 -0
- package/package.json +61 -4
- package/src/Flow.tsx +684 -0
- package/src/components.tsx +180 -0
- package/src/frame.ts +203 -0
- package/src/geometry.ts +274 -0
- package/src/hooks.tsx +121 -0
- package/src/index.ts +21 -0
- package/src/keymap.tsx +45 -0
- package/src/navigation.ts +86 -0
- package/src/nodes.tsx +86 -0
- package/src/raster.ts +453 -0
- package/src/store.ts +240 -0
- package/src/theme.ts +61 -0
- package/src/types.ts +130 -0
- package/src/vendor/xyflow/LICENSE +21 -0
- package/src/vendor/xyflow/README.md +20 -0
- package/src/vendor/xyflow/bezier.ts +102 -0
- package/src/vendor/xyflow/changes.ts +177 -0
- package/src/vendor/xyflow/smoothstep.ts +219 -0
- package/tsconfig.json +22 -0
package/src/hooks.tsx
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useState,
|
|
6
|
+
useSyncExternalStore,
|
|
7
|
+
type Dispatch,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
type SetStateAction,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { FlowStore } from "./store.ts";
|
|
12
|
+
import type { Detail, Edge, EdgeChange, Node, NodeChange, Viewport, XY } from "./types.ts";
|
|
13
|
+
import { applyEdgeChanges, applyNodeChanges } from "./vendor/xyflow/changes.ts";
|
|
14
|
+
|
|
15
|
+
export const StoreContext = createContext<FlowStore | null>(null);
|
|
16
|
+
/** The node a `<Handle>` belongs to. */
|
|
17
|
+
export const NodeIdContext = createContext<string | null>(null);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Shares one canvas's store with components outside `<Flow>` (a side panel that calls
|
|
21
|
+
* `fitView`, a toolbar): wrap both in it. Without it, `<Flow>` keeps its own.
|
|
22
|
+
*/
|
|
23
|
+
export function FlowProvider({
|
|
24
|
+
children,
|
|
25
|
+
defaultViewport,
|
|
26
|
+
fitView,
|
|
27
|
+
}: {
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
defaultViewport?: Viewport;
|
|
30
|
+
fitView?: boolean;
|
|
31
|
+
}) {
|
|
32
|
+
const [store] = useState(() => new FlowStore({ viewport: defaultViewport, fitView }));
|
|
33
|
+
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useStore(): FlowStore {
|
|
37
|
+
const store = useContext(StoreContext);
|
|
38
|
+
if (!store) throw new Error("useFlow and the canvas components need a <Flow> or <FlowProvider>");
|
|
39
|
+
return store;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Re-renders on every change of the store; returns it. */
|
|
43
|
+
export function useStoreVersion(): FlowStore {
|
|
44
|
+
const store = useStore();
|
|
45
|
+
useSyncExternalStore(store.subscribe, store.getVersion);
|
|
46
|
+
return store;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `useState` for nodes, with the `onNodesChange` that applies the canvas's changes. */
|
|
50
|
+
export function useNodesState<N extends Node = Node>(
|
|
51
|
+
initial: N[],
|
|
52
|
+
): [N[], Dispatch<SetStateAction<N[]>>, (changes: NodeChange<N>[]) => void] {
|
|
53
|
+
const [nodes, setNodes] = useState(initial);
|
|
54
|
+
const onNodesChange = useCallback(
|
|
55
|
+
(changes: NodeChange<N>[]) => setNodes((current) => applyNodeChanges(changes, current)),
|
|
56
|
+
[],
|
|
57
|
+
);
|
|
58
|
+
return [nodes, setNodes, onNodesChange];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `useState` for edges, with the `onEdgesChange` that applies the canvas's changes. */
|
|
62
|
+
export function useEdgesState<E extends Edge = Edge>(
|
|
63
|
+
initial: E[],
|
|
64
|
+
): [E[], Dispatch<SetStateAction<E[]>>, (changes: EdgeChange<E>[]) => void] {
|
|
65
|
+
const [edges, setEdges] = useState(initial);
|
|
66
|
+
const onEdgesChange = useCallback(
|
|
67
|
+
(changes: EdgeChange<E>[]) => setEdges((current) => applyEdgeChanges(changes, current)),
|
|
68
|
+
[],
|
|
69
|
+
);
|
|
70
|
+
return [edges, setEdges, onEdgesChange];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type FlowInstance = {
|
|
74
|
+
getNodes(): readonly Node[];
|
|
75
|
+
getNode(id: string): Node | undefined;
|
|
76
|
+
getEdges(): readonly Edge[];
|
|
77
|
+
getViewport(): Viewport;
|
|
78
|
+
setViewport(viewport: Viewport): void;
|
|
79
|
+
/** The closest zoom level at which every node is seen, centered. */
|
|
80
|
+
fitView(): void;
|
|
81
|
+
zoomIn(): void;
|
|
82
|
+
zoomOut(): void;
|
|
83
|
+
/** Centers the view on a point in flow coordinates. */
|
|
84
|
+
setCenter(point: XY, zoom?: number): void;
|
|
85
|
+
/** Pans just enough to show node `id`. */
|
|
86
|
+
reveal(id: string): void;
|
|
87
|
+
/** Proposes removing these nodes (with their edges) and edges. */
|
|
88
|
+
deleteElements(elements: { nodes?: readonly string[]; edges?: readonly string[] }): void;
|
|
89
|
+
/** Proposes selecting exactly these nodes and edges. */
|
|
90
|
+
select(elements: { nodes?: readonly string[]; edges?: readonly string[] }): void;
|
|
91
|
+
/** The canvas point at the center of the view, in flow coordinates. */
|
|
92
|
+
center(): XY;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** The canvas's controls, for a component inside `<Flow>` or `<FlowProvider>`. */
|
|
96
|
+
export function useFlow(): FlowInstance {
|
|
97
|
+
const store = useStore();
|
|
98
|
+
const [instance] = useState<FlowInstance>(() => ({
|
|
99
|
+
getNodes: () => store.nodes,
|
|
100
|
+
getNode: (id) => store.nodes.find((n) => n.id === id),
|
|
101
|
+
getEdges: () => store.edges,
|
|
102
|
+
getViewport: () => store.viewport,
|
|
103
|
+
setViewport: (viewport) => store.setViewport(viewport),
|
|
104
|
+
fitView: () => store.fitView(),
|
|
105
|
+
zoomIn: () => store.zoom(1),
|
|
106
|
+
zoomOut: () => store.zoom(-1),
|
|
107
|
+
setCenter: (point, zoom) => store.setCenter(point, zoom),
|
|
108
|
+
reveal: (id) => store.reveal(id),
|
|
109
|
+
deleteElements: (elements) => store.deleteElements(elements),
|
|
110
|
+
select: (elements) => store.select(elements),
|
|
111
|
+
center: () =>
|
|
112
|
+
store.toFlow({ x: Math.floor(store.size.width / 2), y: Math.floor(store.size.height / 2) }),
|
|
113
|
+
}));
|
|
114
|
+
return instance;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The viewport and the detail it draws nodes at, re-rendering when they change. */
|
|
118
|
+
export function useViewport(): Viewport & { detail: Detail } {
|
|
119
|
+
const store = useStoreVersion();
|
|
120
|
+
return { ...store.viewport, detail: store.detail };
|
|
121
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@luciole-sh/flow-graph`: node graphs in the terminal, after React Flow. Use it in a Client
|
|
3
|
+
* Component (`"use client"`): pan, zoom, drag and selection stay on the Client.
|
|
4
|
+
*/
|
|
5
|
+
export { Flow, type FlowProps } from "./Flow.tsx";
|
|
6
|
+
export { Background, Controls, MiniMap, Panel, type PanelPosition } from "./components.tsx";
|
|
7
|
+
export { DefaultNode, GroupNode, Handle } from "./nodes.tsx";
|
|
8
|
+
export {
|
|
9
|
+
FlowProvider,
|
|
10
|
+
useEdgesState,
|
|
11
|
+
useFlow,
|
|
12
|
+
useNodesState,
|
|
13
|
+
useViewport,
|
|
14
|
+
type FlowInstance,
|
|
15
|
+
} from "./hooks.tsx";
|
|
16
|
+
export { addEdge, applyEdgeChanges, applyNodeChanges, getEdgeId } from "./vendor/xyflow/changes.ts";
|
|
17
|
+
export { composeFrame, hitTest, type EdgeRoute, type EdgeType, type Frame } from "./frame.ts";
|
|
18
|
+
export { ZOOMS, detailFor, fitViewport, flowBounds } from "./geometry.ts";
|
|
19
|
+
export { Grid, type BackgroundVariant, type EdgeStyle } from "./raster.ts";
|
|
20
|
+
export { defaultTheme, type FlowTheme } from "./theme.ts";
|
|
21
|
+
export type * from "./types.ts";
|
package/src/keymap.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import type { CliRenderer, KeyEvent, Renderable } from "@opentui/core";
|
|
3
|
+
import type { Keymap } from "@opentui/keymap";
|
|
4
|
+
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui";
|
|
5
|
+
import { KeymapProvider, useKeymap } from "@opentui/keymap/react";
|
|
6
|
+
import { useRenderer } from "@opentui/react";
|
|
7
|
+
|
|
8
|
+
type OpenTuiKeymap = Keymap<Renderable, KeyEvent>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The keymap of the application's `<KeymapProvider>`, or null without one.
|
|
12
|
+
* `@opentui/keymap/react` exports no context, only `useKeymap`, which throws when there is
|
|
13
|
+
* no provider: the throw comes after its `useContext`, so hooks keep their order.
|
|
14
|
+
*/
|
|
15
|
+
function useOptionalKeymap(): OpenTuiKeymap | null {
|
|
16
|
+
try {
|
|
17
|
+
return useKeymap();
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A keymap listens to its renderer for good (it has no dispose): one per renderer, shared
|
|
24
|
+
// by every canvas that has no provider, however often they mount.
|
|
25
|
+
const ownKeymaps = new WeakMap<CliRenderer, OpenTuiKeymap>();
|
|
26
|
+
function keymapFor(renderer: CliRenderer): OpenTuiKeymap {
|
|
27
|
+
const known = ownKeymaps.get(renderer);
|
|
28
|
+
if (known) return known;
|
|
29
|
+
const keymap = createDefaultOpenTuiKeymap(renderer);
|
|
30
|
+
ownKeymaps.set(renderer, keymap);
|
|
31
|
+
return keymap;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The canvas's bindings go through `useBindings` in every case. Inside the application's
|
|
36
|
+
* `<KeymapProvider>` (luciole's Shell installs one), they join its layers: `<KeyHelp>`
|
|
37
|
+
* lists them, and the application's own layers can shadow them. Without one, the canvas
|
|
38
|
+
* provides a keymap of its own on the renderer.
|
|
39
|
+
*/
|
|
40
|
+
export function FlowKeymap({ children }: { children: ReactNode }) {
|
|
41
|
+
const outer = useOptionalKeymap();
|
|
42
|
+
const renderer = useRenderer();
|
|
43
|
+
const [own] = useState(() => (outer ? null : keymapFor(renderer)));
|
|
44
|
+
return own ? <KeymapProvider keymap={own}>{children}</KeymapProvider> : children;
|
|
45
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moving the selection with the keyboard: in reading order (columns left to right, a
|
|
3
|
+
* column top to bottom, as a left-to-right flow reads), along the edges (downstream,
|
|
4
|
+
* upstream), among siblings, or to the nearest node on one side. Pure functions of the
|
|
5
|
+
* graph and the nodes' positions.
|
|
6
|
+
*/
|
|
7
|
+
import type { Edge, Node, Position, XY } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
type Located = { id: string; at: XY };
|
|
10
|
+
|
|
11
|
+
const byColumn = (a: Located, b: Located) => a.at.x - b.at.x || a.at.y - b.at.y;
|
|
12
|
+
const byRow = (a: Located, b: Located) => a.at.y - b.at.y || a.at.x - b.at.x;
|
|
13
|
+
|
|
14
|
+
/** Selectable, visible nodes with their positions, in reading order. */
|
|
15
|
+
export function readingOrder(nodes: readonly Node[], positions: ReadonlyMap<string, XY>) {
|
|
16
|
+
return nodes
|
|
17
|
+
.filter((n) => !n.hidden && n.selectable !== false && n.type !== "group")
|
|
18
|
+
.map((n) => ({ id: n.id, at: positions.get(n.id) ?? n.position }))
|
|
19
|
+
.sort(byColumn);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The next (`1`) or previous (`-1`) node after `current` in reading order, cycling. */
|
|
23
|
+
export function cycle(order: readonly Located[], current: string | undefined, step: 1 | -1) {
|
|
24
|
+
if (order.length === 0) return undefined;
|
|
25
|
+
const index = order.findIndex((n) => n.id === current);
|
|
26
|
+
if (index < 0) return (step === 1 ? order[0] : order.at(-1))?.id;
|
|
27
|
+
return order[(index + step + order.length) % order.length]?.id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The first node `current`'s edges lead to (`down`) or come from (`up`), top first. */
|
|
31
|
+
export function follow(
|
|
32
|
+
edges: readonly Edge[],
|
|
33
|
+
order: readonly Located[],
|
|
34
|
+
current: string,
|
|
35
|
+
direction: "down" | "up",
|
|
36
|
+
) {
|
|
37
|
+
const ids = new Set(
|
|
38
|
+
edges
|
|
39
|
+
.filter((e) => !e.hidden && (direction === "down" ? e.source : e.target) === current)
|
|
40
|
+
.map((e) => (direction === "down" ? e.target : e.source)),
|
|
41
|
+
);
|
|
42
|
+
return order.filter((n) => ids.has(n.id)).sort(byRow)[0]?.id;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The next (`1`) or previous (`-1`) sibling of `current`: another node an upstream node
|
|
47
|
+
* of `current` leads to, top to bottom, cycling.
|
|
48
|
+
*/
|
|
49
|
+
export function sibling(
|
|
50
|
+
edges: readonly Edge[],
|
|
51
|
+
order: readonly Located[],
|
|
52
|
+
current: string,
|
|
53
|
+
step: 1 | -1,
|
|
54
|
+
) {
|
|
55
|
+
const parents = new Set(edges.filter((e) => e.target === current).map((e) => e.source));
|
|
56
|
+
const ids = new Set(edges.filter((e) => parents.has(e.source)).map((e) => e.target));
|
|
57
|
+
// A root's siblings are the other roots.
|
|
58
|
+
if (parents.size === 0) {
|
|
59
|
+
const targets = new Set(edges.map((e) => e.target));
|
|
60
|
+
for (const n of order) if (!targets.has(n.id)) ids.add(n.id);
|
|
61
|
+
}
|
|
62
|
+
const list = order.filter((n) => ids.has(n.id)).sort(byRow);
|
|
63
|
+
return list.length > 1 ? cycle(list, current, step) : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The nearest node on `side` of `current`: within a 45° cone first, by distance with rows
|
|
68
|
+
* counted twice (a row is about two columns tall).
|
|
69
|
+
*/
|
|
70
|
+
export function nearest(order: readonly Located[], current: string, side: Position) {
|
|
71
|
+
const from = order.find((n) => n.id === current);
|
|
72
|
+
if (!from) return undefined;
|
|
73
|
+
let best: { id: string; score: number } | undefined;
|
|
74
|
+
for (const n of order) {
|
|
75
|
+
if (n.id === current) continue;
|
|
76
|
+
const dx = n.at.x - from.at.x;
|
|
77
|
+
const dy = (n.at.y - from.at.y) * 2;
|
|
78
|
+
const forward = side === "right" ? dx : side === "left" ? -dx : side === "bottom" ? dy : -dy;
|
|
79
|
+
const across = side === "left" || side === "right" ? Math.abs(dy) : Math.abs(dx);
|
|
80
|
+
if (forward <= 0) continue;
|
|
81
|
+
// Outside the cone costs as much again as the distance across.
|
|
82
|
+
const score = forward + across * (across > forward ? 2 : 1);
|
|
83
|
+
if (!best || score < best.score) best = { id: n.id, score };
|
|
84
|
+
}
|
|
85
|
+
return best?.id;
|
|
86
|
+
}
|
package/src/nodes.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useContext, useEffect, useId } from "react";
|
|
2
|
+
import { labelOf } from "./geometry.ts";
|
|
3
|
+
import { NodeIdContext, useStore } from "./hooks.tsx";
|
|
4
|
+
import { useFlowTheme } from "./theme.ts";
|
|
5
|
+
import type { HandleType, NodeComponent, NodeProps, Position } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Declares where a node's edges attach: a node component renders one per handle. It
|
|
9
|
+
* draws nothing itself: the canvas marks handles on the node's border when useful (the
|
|
10
|
+
* selected node's sources, every target while connecting). Handles sharing a side are
|
|
11
|
+
* spread along it, in the order they are rendered.
|
|
12
|
+
*/
|
|
13
|
+
export function Handle({
|
|
14
|
+
type,
|
|
15
|
+
position,
|
|
16
|
+
id = null,
|
|
17
|
+
}: {
|
|
18
|
+
type: HandleType;
|
|
19
|
+
position: Position;
|
|
20
|
+
id?: string | null;
|
|
21
|
+
}) {
|
|
22
|
+
const nodeId = useContext(NodeIdContext);
|
|
23
|
+
const store = useStore();
|
|
24
|
+
const key = useId();
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!nodeId) return;
|
|
27
|
+
store.addHandle(nodeId, key, { id, type, position });
|
|
28
|
+
return () => store.removeHandle(nodeId, key);
|
|
29
|
+
}, [store, nodeId, key, id, type, position]);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A label in a rounded box, one row tall; its label only when compact. */
|
|
34
|
+
export function DefaultNode({ data, id, selected, detail, connectTarget, type }: NodeProps) {
|
|
35
|
+
const theme = useFlowTheme();
|
|
36
|
+
const label = labelOf({ id, data, position: { x: 0, y: 0 } });
|
|
37
|
+
const color = selected ? theme.selected : connectTarget ? theme.connect : theme.border;
|
|
38
|
+
if (detail === "compact")
|
|
39
|
+
return (
|
|
40
|
+
<text
|
|
41
|
+
selectable={false}
|
|
42
|
+
wrapMode="none"
|
|
43
|
+
fg={selected || connectTarget ? color : theme.text}
|
|
44
|
+
bg={theme.panelBg}
|
|
45
|
+
>
|
|
46
|
+
{` ${label} `}
|
|
47
|
+
</text>
|
|
48
|
+
);
|
|
49
|
+
// An input starts the flow and an output ends it: their border says so.
|
|
50
|
+
return (
|
|
51
|
+
<box
|
|
52
|
+
border
|
|
53
|
+
borderStyle={type === "input" ? "double" : type === "output" ? "heavy" : "rounded"}
|
|
54
|
+
borderColor={color}
|
|
55
|
+
paddingX={1}
|
|
56
|
+
backgroundColor={theme.panelBg}
|
|
57
|
+
>
|
|
58
|
+
<text selectable={false} fg={theme.text} wrapMode="none">
|
|
59
|
+
{label}
|
|
60
|
+
</text>
|
|
61
|
+
</box>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A frame other nodes sit in (`parentId`), sized by the node's `width` and `height`. */
|
|
66
|
+
export function GroupNode({ data, id, selected, detail, width, height }: NodeProps) {
|
|
67
|
+
const theme = useFlowTheme();
|
|
68
|
+
const label = labelOf({ id, data, position: { x: 0, y: 0 } });
|
|
69
|
+
return (
|
|
70
|
+
<box
|
|
71
|
+
width={width}
|
|
72
|
+
height={height}
|
|
73
|
+
border
|
|
74
|
+
borderStyle="single"
|
|
75
|
+
borderColor={selected ? theme.selected : theme.group}
|
|
76
|
+
title={detail === "full" ? ` ${label} ` : undefined}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const builtinNodeTypes: Record<string, NodeComponent> = {
|
|
82
|
+
default: DefaultNode,
|
|
83
|
+
input: DefaultNode,
|
|
84
|
+
output: DefaultNode,
|
|
85
|
+
group: GroupNode,
|
|
86
|
+
};
|