@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/README.md
CHANGED
|
@@ -1,3 +1,672 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @luciole-sh/flow-graph
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@luciole-sh/flow-graph` draws node graphs in your terminal, with the names and the shape of
|
|
4
|
+
[React Flow](https://reactflow.dev). It is published on npm, and it renders through
|
|
5
|
+
[OpenTUI](https://github.com/anomalyco/opentui).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
It runs on Node 26.4 or newer, the version OpenTUI needs, or on Bun 1.3 or newer. The package
|
|
10
|
+
has no dependency of its own. React and OpenTUI are peers, so your project keeps one copy of
|
|
11
|
+
each.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bun add @luciole-sh/flow-graph @opentui/core @opentui/keymap @opentui/react react
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The package is ESM only and ships its type declarations.
|
|
18
|
+
|
|
19
|
+
## Run a first graph
|
|
20
|
+
|
|
21
|
+
In an empty project, add this `tsconfig.json`. It compiles JSX for OpenTUI.
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"compilerOptions": {
|
|
26
|
+
"module": "ESNext",
|
|
27
|
+
"moduleResolution": "Bundler",
|
|
28
|
+
"jsx": "react-jsx",
|
|
29
|
+
"jsxImportSource": "@opentui/react",
|
|
30
|
+
"strict": true,
|
|
31
|
+
"noEmit": true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Save this program as `main.tsx`, then run `bun main.tsx`. <kbd>Ctrl</kbd>+<kbd>C</kbd> quits.
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { createCliRenderer } from "@opentui/core";
|
|
40
|
+
import { createRoot } from "@opentui/react";
|
|
41
|
+
import {
|
|
42
|
+
addEdge,
|
|
43
|
+
Background,
|
|
44
|
+
Controls,
|
|
45
|
+
Flow,
|
|
46
|
+
MiniMap,
|
|
47
|
+
useEdgesState,
|
|
48
|
+
useNodesState,
|
|
49
|
+
type Edge,
|
|
50
|
+
type Node,
|
|
51
|
+
} from "@luciole-sh/flow-graph";
|
|
52
|
+
|
|
53
|
+
const initialNodes: Node[] = [
|
|
54
|
+
{ id: "checkout", type: "input", position: { x: 0, y: 3 }, data: { label: "checkout" } },
|
|
55
|
+
{ id: "lint", position: { x: 20, y: 0 }, data: { label: "lint" } },
|
|
56
|
+
{ id: "test", position: { x: 20, y: 6 }, data: { label: "test" } },
|
|
57
|
+
{ id: "build", type: "output", position: { x: 40, y: 3 }, data: { label: "build" } },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const initialEdges: Edge[] = [
|
|
61
|
+
{ id: "checkout-lint", source: "checkout", target: "lint" },
|
|
62
|
+
{ id: "checkout-test", source: "checkout", target: "test", label: "fast" },
|
|
63
|
+
{ id: "lint-build", source: "lint", target: "build" },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function Pipeline() {
|
|
67
|
+
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
|
68
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
69
|
+
return (
|
|
70
|
+
<Flow
|
|
71
|
+
nodes={nodes}
|
|
72
|
+
edges={edges}
|
|
73
|
+
onNodesChange={onNodesChange}
|
|
74
|
+
onEdgesChange={onEdgesChange}
|
|
75
|
+
onConnect={(connection) => setEdges((current) => addEdge(connection, current))}
|
|
76
|
+
fitView
|
|
77
|
+
>
|
|
78
|
+
<Background />
|
|
79
|
+
<Controls />
|
|
80
|
+
<MiniMap />
|
|
81
|
+
</Flow>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const renderer = await createCliRenderer();
|
|
86
|
+
createRoot(renderer).render(<Pipeline />);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The first frame is already fitted to your terminal:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
· · · · · · · · ·
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
· · · · ╭──────╮ · · · ·
|
|
99
|
+
╭──▶│ lint │──────╮
|
|
100
|
+
│ ╰──────╯ │
|
|
101
|
+
╔══════════╗ │ │ ┏━━━━━━━┓
|
|
102
|
+
· ║ checkout ║────┤ · · ╰────▶┃ build ┃ ·
|
|
103
|
+
╚══════════╝ │ ┗━━━━━━━┛
|
|
104
|
+
fast ╭──────╮ ⡤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⢤
|
|
105
|
+
╰──▶│ test │ ⡇ ⢸
|
|
106
|
+
· · · · ╰──────╯ · ⡇ ⢠⣤⣤⡄ ⢸
|
|
107
|
+
⡇ ⣤⣤⣤⣤ ⠸⠿⠿⠇ ⣤⣤⣤⡄ ⢸
|
|
108
|
+
⡇ ⠿⠿⠿⠿ ⢠⣤⣤⡄ ⠿⠿⠿⠇ ⢸
|
|
109
|
+
⡇ ⠸⠿⠿⠇ ⢸
|
|
110
|
+
· · · · · · ⡇ ⢸
|
|
111
|
+
+ - fit full ⠓⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
_The program above, in a 72×20 terminal. The frame is captured from a real PTY by
|
|
115
|
+
[`website/scripts/capture.py`](https://github.com/sykar-f/luciole/blob/v0.1.0/website/scripts/capture.py)
|
|
116
|
+
(`python3 website/scripts/capture.py flow-graph`), and the program is
|
|
117
|
+
[`example/main.tsx`](https://github.com/sykar-f/luciole/blob/v0.1.0/packages/flow-graph/example/main.tsx)._
|
|
118
|
+
|
|
119
|
+
The graph is yours to edit. Drag a node, or select one and move it with the keys below.
|
|
120
|
+
Drag from the dot on a selected node to connect it to another node.
|
|
121
|
+
|
|
122
|
+
### Run the example from a clone
|
|
123
|
+
|
|
124
|
+
The same program is in the repository, in `packages/flow-graph/example/`. From the root of a
|
|
125
|
+
clone, run:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
bun install
|
|
129
|
+
bun packages/flow-graph/example/main.tsx
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`bun run check` type-checks that program, and `tests/readme-examples.test.ts` fails when this
|
|
133
|
+
page and the file differ. The same test type-checks every other example of this page.
|
|
134
|
+
|
|
135
|
+
## Use it in a luciole app
|
|
136
|
+
|
|
137
|
+
In a luciole app, use `<Flow>` in a Client Component. Pan, zoom, drag and selection stay on
|
|
138
|
+
the Client. Only what your app sends, such as a Server Function called from `onNodesChange`
|
|
139
|
+
or `onConnect`, reaches the Server.
|
|
140
|
+
|
|
141
|
+
[`examples/flow`](https://github.com/sykar-f/luciole/tree/v0.1.0/examples/flow) is a full app: a
|
|
142
|
+
CI pipeline editor with custom nodes, a side panel and live runs on the Server. The framework's
|
|
143
|
+
documentation is at <https://luciole.sh/docs/>.
|
|
144
|
+
|
|
145
|
+
## Components
|
|
146
|
+
|
|
147
|
+
Every name on this page is imported from `@luciole-sh/flow-graph`. Coordinates are terminal
|
|
148
|
+
cells: `x` counts columns and `y` counts rows. Colours are `#rrggbb` strings.
|
|
149
|
+
|
|
150
|
+
### `<Flow>`
|
|
151
|
+
|
|
152
|
+
The canvas. It is controlled: you own `nodes` and `edges`, and the canvas proposes changes
|
|
153
|
+
through `onNodesChange` and `onEdgesChange`. Without these callbacks, nodes stay where they are.
|
|
154
|
+
|
|
155
|
+
| Prop | Type | Default | Meaning |
|
|
156
|
+
| ------------------ | ------------------------------------ | ------------------------- | ------------------------------------------------------------------------ |
|
|
157
|
+
| `nodes` | `N[]` | required | The nodes. |
|
|
158
|
+
| `edges` | `E[]` | required | The edges. |
|
|
159
|
+
| `onNodesChange` | `(changes: NodeChange<N>[]) => void` | | Receives drags, selections and removals. Pass it to `applyNodeChanges`. |
|
|
160
|
+
| `onEdgesChange` | `(changes: EdgeChange<E>[]) => void` | | The same for edges. |
|
|
161
|
+
| `onConnect` | `(connection: Connection) => void` | | A connection made from a handle, or with the keyboard. |
|
|
162
|
+
| `onNodeClick` | `(node: N) => void` | | A click on a node. |
|
|
163
|
+
| `onEdgeClick` | `(edge: E) => void` | | A click on an edge. |
|
|
164
|
+
| `onPaneClick` | `(at: XY) => void` | | A click on the empty canvas, in flow coordinates. |
|
|
165
|
+
| `onNodeDragStop` | `(nodes: N[]) => void` | | The end of a mouse drag, with the moved nodes at their new positions. |
|
|
166
|
+
| `onViewportChange` | `(viewport: Viewport) => void` | | The view moved or zoomed. |
|
|
167
|
+
| `nodeTypes` | `Record<string, NodeComponent<N>>` | | Your node components, by `node.type`. |
|
|
168
|
+
| `edgeTypes` | `Record<string, EdgeType>` | | Your edge styles and routes, by `edge.type`. |
|
|
169
|
+
| `defaultEdgeType` | `string` | `"smoothstep"` | The type of an edge that names none. |
|
|
170
|
+
| `fitView` | `boolean` | `false` | Fits every node in view once the canvas has a size. |
|
|
171
|
+
| `defaultViewport` | `Viewport` | `{ x: 2, y: 1, zoom: 1 }` | The first view. |
|
|
172
|
+
| `keyboard` | `boolean` | `true` | Turns the key bindings off while a field elsewhere takes the keys. |
|
|
173
|
+
| `braille` | `boolean` | `true` | Draws curves in braille. When `false`, curves are drawn as `smoothstep`. |
|
|
174
|
+
| `theme` | `Partial<FlowTheme>` | `defaultTheme` | Overrides colours of the canvas. |
|
|
175
|
+
| `id` | `string` | | A name for the canvas. |
|
|
176
|
+
| `children` | `ReactNode` | | `<Background>`, `<MiniMap>`, `<Controls>` and `<Panel>`. |
|
|
177
|
+
|
|
178
|
+
`FlowProps<N, E>` is the type of these props. `N` extends `Node` and `E` extends `Edge`, so
|
|
179
|
+
the callbacks receive your own node and edge types:
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
import { Flow, type FlowProps, type Node } from "@luciole-sh/flow-graph";
|
|
183
|
+
|
|
184
|
+
type Step = Node<{ label: string; command: string }>;
|
|
185
|
+
|
|
186
|
+
export function Steps({ nodes, edges }: Pick<FlowProps<Step>, "nodes" | "edges">) {
|
|
187
|
+
return (
|
|
188
|
+
<Flow nodes={nodes} edges={edges} onNodeClick={(step) => console.log(step.data.command)} />
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`<Flow>` raises no error of its own. A node of an unknown `type` is drawn as a `default` node,
|
|
194
|
+
and an edge of an unknown `type` as a `smoothstep` edge.
|
|
195
|
+
|
|
196
|
+
### `<Background>`
|
|
197
|
+
|
|
198
|
+
Marks every `gap` flow cells, under the edges.
|
|
199
|
+
|
|
200
|
+
| Prop | Type | Default | Meaning |
|
|
201
|
+
| --------- | ------------------- | ------------------ | ------------------------ |
|
|
202
|
+
| `variant` | `BackgroundVariant` | `"dots"` | The mark. |
|
|
203
|
+
| `gap` | `XY` | `{ x: 8, y: 4 }` | The spacing, in cells. |
|
|
204
|
+
| `color` | `string` | the theme's colour | The colour of the marks. |
|
|
205
|
+
|
|
206
|
+
`BackgroundVariant` is `"dots"`, `"lines"` or `"cross"`.
|
|
207
|
+
|
|
208
|
+
### `<Controls>`
|
|
209
|
+
|
|
210
|
+
Zoom in, zoom out, fit the view, and show the current level.
|
|
211
|
+
|
|
212
|
+
| Prop | Type | Default | Meaning |
|
|
213
|
+
| ---------- | --------------- | --------------- | ------------------------------ |
|
|
214
|
+
| `position` | `PanelPosition` | `"bottom-left"` | The corner or edge it sits on. |
|
|
215
|
+
|
|
216
|
+
### `<MiniMap>`
|
|
217
|
+
|
|
218
|
+
Every node in braille, with the part in view outlined. A click on it centres the view there.
|
|
219
|
+
|
|
220
|
+
| Prop | Type | Default | Meaning |
|
|
221
|
+
| ---------- | --------------- | ---------------- | ---------------------- |
|
|
222
|
+
| `position` | `PanelPosition` | `"bottom-right"` | Where it sits. |
|
|
223
|
+
| `width` | `number` | `24` | Its width, in columns. |
|
|
224
|
+
| `height` | `number` | `8` | Its height, in rows. |
|
|
225
|
+
|
|
226
|
+
### `<Panel>`
|
|
227
|
+
|
|
228
|
+
Your own content over the canvas. A click on a panel does not select or pan the canvas.
|
|
229
|
+
|
|
230
|
+
| Prop | Type | Default | Meaning |
|
|
231
|
+
| ---------- | --------------- | ------------ | -------------- |
|
|
232
|
+
| `position` | `PanelPosition` | `"top-left"` | Where it sits. |
|
|
233
|
+
| `children` | `ReactNode` | required | What it shows. |
|
|
234
|
+
|
|
235
|
+
`PanelPosition` is `"top-left"`, `"top-center"`, `"top-right"`, `"bottom-left"`,
|
|
236
|
+
`"bottom-center"` or `"bottom-right"`.
|
|
237
|
+
|
|
238
|
+
### `<FlowProvider>`
|
|
239
|
+
|
|
240
|
+
Shares one canvas with components outside `<Flow>`, such as a side panel that calls `fitView`.
|
|
241
|
+
Wrap the panel and the `<Flow>` in it. Without it, `<Flow>` keeps its own state.
|
|
242
|
+
|
|
243
|
+
| Prop | Type | Default | Meaning |
|
|
244
|
+
| ----------------- | ----------- | ------------------------- | ------------------------------------- |
|
|
245
|
+
| `children` | `ReactNode` | required | The panel and the `<Flow>`. |
|
|
246
|
+
| `defaultViewport` | `Viewport` | `{ x: 2, y: 1, zoom: 1 }` | The first view. |
|
|
247
|
+
| `fitView` | `boolean` | `false` | Fits every node in view at the start. |
|
|
248
|
+
|
|
249
|
+
`<Background>`, `<Controls>`, `<MiniMap>` and `<Handle>` read the canvas they belong to.
|
|
250
|
+
Rendered outside a `<Flow>` or a `<FlowProvider>`, they throw
|
|
251
|
+
`useFlow and the canvas components need a <Flow> or <FlowProvider>`.
|
|
252
|
+
|
|
253
|
+
## Nodes and edges
|
|
254
|
+
|
|
255
|
+
### `Node`
|
|
256
|
+
|
|
257
|
+
A node as your app keeps it. `Node<Data>` types its `data`, which defaults to
|
|
258
|
+
`Record<string, unknown>`.
|
|
259
|
+
|
|
260
|
+
| Field | Type | Default | Meaning |
|
|
261
|
+
| ---------------------------------------- | ---------- | ----------- | ------------------------------------------------------------------------------------ |
|
|
262
|
+
| `id` | `string` | required | Unique among the nodes. |
|
|
263
|
+
| `position` | `XY` | required | Its top-left cell, relative to its parent's when it has `parentId`. |
|
|
264
|
+
| `data` | `Data` | required | Your data. The built-in nodes show `data.label`, or the `id` without one. |
|
|
265
|
+
| `type` | `string` | `"default"` | A key of `nodeTypes`, or `default`, `input`, `output` or `group`. |
|
|
266
|
+
| `selected`, `dragging`, `hidden` | `boolean` | `false` | Set by the changes you apply. A hidden node is not drawn. |
|
|
267
|
+
| `parentId` | `string` | | The `group` node it sits in. |
|
|
268
|
+
| `width`, `height` | `number` | measured | A fixed size in cells. A `group` node needs both. |
|
|
269
|
+
| `sourcePosition` | `Position` | `"right"` | Where its edges leave, when it has no `<Handle>`. |
|
|
270
|
+
| `targetPosition` | `Position` | `"left"` | Where its edges arrive, when it has no `<Handle>`. |
|
|
271
|
+
| `draggable`, `selectable`, `connectable` | `boolean` | `true` | Whether you can drag it, select it, or start a connection from it. |
|
|
272
|
+
| `zIndex` | `number` | `0` | Its drawing order. A group is drawn under its children, the selection over the rest. |
|
|
273
|
+
| `color` | `string` | the theme's | Its colour where the canvas draws it itself: a dot when zoomed out, the minimap. |
|
|
274
|
+
|
|
275
|
+
`Position` is the side of a node: `"left"`, `"right"`, `"top"` or `"bottom"`. `XY` is
|
|
276
|
+
`{ x, y }`, and `Rect` is `{ x, y, width, height }`, both in cells.
|
|
277
|
+
|
|
278
|
+
### `Edge`
|
|
279
|
+
|
|
280
|
+
An edge from a source node to a target node. `Edge<Data>` types its `data`.
|
|
281
|
+
|
|
282
|
+
| Field | Type | Default | Meaning |
|
|
283
|
+
| ------------------------------ | ---------------- | ----------------- | ---------------------------------------------------------------------- |
|
|
284
|
+
| `id` | `string` | required | Unique among the edges. |
|
|
285
|
+
| `source`, `target` | `string` | required | The ids of the nodes it joins. |
|
|
286
|
+
| `sourceHandle`, `targetHandle` | `string \| null` | the first handle | The `id` of a `<Handle>`, or the node's first handle of that type. |
|
|
287
|
+
| `type` | `string` | `defaultEdgeType` | A key of `edgeTypes`, or `smoothstep`, `step`, `straight` or `bezier`. |
|
|
288
|
+
| `label` | `string` | | Text drawn on the edge. |
|
|
289
|
+
| `animated` | `boolean` | `false` | Dashes that run from the source to the target. |
|
|
290
|
+
| `selected`, `hidden` | `boolean` | `false` | Set by the changes you apply. A hidden edge is not drawn. |
|
|
291
|
+
| `markerEnd` | `MarkerType` | `"arrow"` | The mark where it reaches the target. |
|
|
292
|
+
| `markerStart` | `MarkerType` | `"none"` | The mark where it leaves the source. |
|
|
293
|
+
| `color` | `string` | the theme's | The colour of its line. |
|
|
294
|
+
| `data` | `Data` | | Your data. |
|
|
295
|
+
|
|
296
|
+
`MarkerType` is `"arrow"` or `"none"`.
|
|
297
|
+
|
|
298
|
+
### `Connection`
|
|
299
|
+
|
|
300
|
+
What `onConnect` receives: `{ source, target, sourceHandle, targetHandle }`. The handles are
|
|
301
|
+
`string | null`, and `null` stands for the node's default handle. Turn a connection into an edge
|
|
302
|
+
with `addEdge`.
|
|
303
|
+
|
|
304
|
+
## Apply the canvas's changes
|
|
305
|
+
|
|
306
|
+
The canvas never edits your nodes. It sends `NodeChange` and `EdgeChange` objects, and you
|
|
307
|
+
apply the ones you accept with `applyNodeChanges` and `applyEdgeChanges`.
|
|
308
|
+
|
|
309
|
+
| Change | Shape | Who sends it |
|
|
310
|
+
| ---------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
311
|
+
| `NodePositionChange` | `{ id, type: "position", position?, dragging? }` | The canvas. A drag sends `dragging: true`, then `false` at the drop. A key move has no `dragging`. |
|
|
312
|
+
| `SelectionChange` | `{ id, type: "select", selected }` | The canvas, on a click or a key, and `select()`. |
|
|
313
|
+
| `RemoveChange` | `{ id, type: "remove" }` | The canvas, on `x` or `Delete`, and `deleteElements()`. |
|
|
314
|
+
| `NodeDimensionsChange` | `{ id, type: "dimensions", dimensions: { width, height } }` | Never the canvas. `applyNodeChanges` leaves the node as it is. |
|
|
315
|
+
| `AddChange<T>` | `{ type: "add", item, index? }` | Your app. The item goes in at `index`, or at the end. |
|
|
316
|
+
| `ReplaceChange<T>` | `{ id, type: "replace", item }` | Your app. The item takes the place of the element `id`. |
|
|
317
|
+
|
|
318
|
+
`NodeChange<N>` is any of the six, with `N` in `add` and `replace`. `EdgeChange<E>` is
|
|
319
|
+
`SelectionChange`, `RemoveChange`, `AddChange<E>` or `ReplaceChange<E>`.
|
|
320
|
+
|
|
321
|
+
This handler applies every change, and saves positions only when a drag ends or a key moves
|
|
322
|
+
a node:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import {
|
|
326
|
+
applyNodeChanges,
|
|
327
|
+
type Node,
|
|
328
|
+
type NodeChange,
|
|
329
|
+
type NodePositionChange,
|
|
330
|
+
} from "@luciole-sh/flow-graph";
|
|
331
|
+
|
|
332
|
+
export function onNodesChange(
|
|
333
|
+
changes: NodeChange[],
|
|
334
|
+
nodes: Node[],
|
|
335
|
+
save: (moves: NodePositionChange[]) => void,
|
|
336
|
+
): Node[] {
|
|
337
|
+
const moves = changes.filter(
|
|
338
|
+
(change): change is NodePositionChange =>
|
|
339
|
+
change.type === "position" && change.dragging !== true,
|
|
340
|
+
);
|
|
341
|
+
if (moves.length > 0) save(moves);
|
|
342
|
+
return applyNodeChanges(changes, nodes);
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Your app sends its own changes through the same function:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import { applyNodeChanges, type Node, type NodeChange } from "@luciole-sh/flow-graph";
|
|
350
|
+
|
|
351
|
+
export const withDeploy = (nodes: Node[]): Node[] => {
|
|
352
|
+
const changes: NodeChange[] = [
|
|
353
|
+
{ type: "add", item: { id: "deploy", position: { x: 60, y: 3 }, data: { label: "deploy" } } },
|
|
354
|
+
{ type: "remove", id: "lint" },
|
|
355
|
+
];
|
|
356
|
+
return applyNodeChanges(changes, nodes);
|
|
357
|
+
};
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Write your own nodes
|
|
361
|
+
|
|
362
|
+
### `NodeComponent` and `NodeProps`
|
|
363
|
+
|
|
364
|
+
A node type is a `NodeComponent<N>`: a function component of `NodeProps<N>`. Register it in
|
|
365
|
+
`nodeTypes` under the name your nodes give in `type`.
|
|
366
|
+
|
|
367
|
+
| Prop | Type | Meaning |
|
|
368
|
+
| ----------------- | ----------- | ----------------------------------------------------------------- |
|
|
369
|
+
| `id` | `string` | The node's id. |
|
|
370
|
+
| `data` | `N["data"]` | The node's `data`. |
|
|
371
|
+
| `type` | `string` | The node's type. |
|
|
372
|
+
| `selected` | `boolean` | Whether the node is selected. |
|
|
373
|
+
| `dragging` | `boolean` | Whether the node is being dragged. |
|
|
374
|
+
| `detail` | `Detail` | The zoom level. At `dot`, the canvas draws the node itself. |
|
|
375
|
+
| `connectTarget` | `boolean` | Whether the node is the proposed target of a keyboard connection. |
|
|
376
|
+
| `width`, `height` | `number` | The node's fixed size in cells, when it has one. |
|
|
377
|
+
|
|
378
|
+
`Detail` is `"full"`, `"compact"` or `"dot"`: what a node shows at each zoom level. The
|
|
379
|
+
built-in `DefaultNode` draws `default`, `input` and `output`, and `GroupNode` draws `group`.
|
|
380
|
+
Wrap or reuse them in your own `nodeTypes`.
|
|
381
|
+
|
|
382
|
+
### `<Handle>`
|
|
383
|
+
|
|
384
|
+
Declares where a custom node's edges attach. Render one per attachment point. A handle draws
|
|
385
|
+
nothing itself. The canvas marks the sources of the selected node, and the targets while you
|
|
386
|
+
connect. Handles that share a side are spread along it, in render order.
|
|
387
|
+
|
|
388
|
+
| Prop | Type | Default | Meaning |
|
|
389
|
+
| ---------- | ---------------- | -------- | -------------------------------------------------------- |
|
|
390
|
+
| `type` | `HandleType` | required | Whether edges leave or arrive here. |
|
|
391
|
+
| `position` | `Position` | required | The side of the node. |
|
|
392
|
+
| `id` | `string \| null` | `null` | Names the handle, for `sourceHandle` and `targetHandle`. |
|
|
393
|
+
|
|
394
|
+
`HandleType` is `"source"` or `"target"`. A `<Handle>` outside a node component does nothing.
|
|
395
|
+
|
|
396
|
+
This node shows a job's status and sends an edge from one of two handles:
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
import { Handle, type Node, type NodeComponent } from "@luciole-sh/flow-graph";
|
|
400
|
+
|
|
401
|
+
type Job = Node<{ label: string; failed: boolean }>;
|
|
402
|
+
|
|
403
|
+
export const JobNode: NodeComponent<Job> = ({ data, selected, detail }) =>
|
|
404
|
+
detail === "compact" ? (
|
|
405
|
+
<text selectable={false}>{data.label}</text>
|
|
406
|
+
) : (
|
|
407
|
+
<box border borderStyle={selected ? "double" : "rounded"} paddingX={1}>
|
|
408
|
+
<text selectable={false}>{`${data.failed ? "✗" : "✓"} ${data.label}`}</text>
|
|
409
|
+
<Handle type="target" position="left" />
|
|
410
|
+
<Handle type="source" position="right" id="next" />
|
|
411
|
+
<Handle type="source" position="bottom" id="retry" />
|
|
412
|
+
</box>
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
export const nodeTypes = { job: JobNode };
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
An edge leaves the bottom when it sets `sourceHandle: "retry"`.
|
|
419
|
+
|
|
420
|
+
## Draw your own edges
|
|
421
|
+
|
|
422
|
+
### `EdgeType`, `EdgeStyle` and `EdgeRoute`
|
|
423
|
+
|
|
424
|
+
An `edgeTypes` entry is an `EdgeType`: an `EdgeStyle` or an `EdgeRoute`.
|
|
425
|
+
|
|
426
|
+
- `EdgeStyle` is a built-in style: `"smoothstep"`, `"step"`, `"straight"` or `"bezier"`.
|
|
427
|
+
- `EdgeRoute` is a function. It receives `{ edge, source, sourcePosition, target, targetPosition }`,
|
|
428
|
+
where `source` and `target` are the anchor cells on the canvas. It returns the corners of the
|
|
429
|
+
path, both ends included.
|
|
430
|
+
|
|
431
|
+
The canvas draws a route in box characters, like a `step` edge. Two corners that share neither a
|
|
432
|
+
row nor a column are joined across, then down.
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
import type { EdgeType } from "@luciole-sh/flow-graph";
|
|
436
|
+
|
|
437
|
+
// Runs two rows under the lower node, then rises to the target.
|
|
438
|
+
const under: EdgeType = ({ source, target }) => {
|
|
439
|
+
const floor = Math.max(source.y, target.y) + 2;
|
|
440
|
+
return [source, { x: source.x, y: floor }, { x: target.x, y: floor }, target];
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
export const edgeTypes: Record<string, EdgeType> = { under, curve: "bezier" };
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Change the colours
|
|
447
|
+
|
|
448
|
+
### `FlowTheme`
|
|
449
|
+
|
|
450
|
+
The canvas's colours. Pass any of them to `<Flow theme>`, and `defaultTheme` gives the others.
|
|
451
|
+
|
|
452
|
+
| Key | Default | Colours |
|
|
453
|
+
| ------------ | --------- | ----------------------------------------------------------------------------------------- |
|
|
454
|
+
| `text` | `#e6edf3` | The labels of the built-in nodes, the buttons of `<Controls>`, and a node drawn as a dot. |
|
|
455
|
+
| `muted` | `#8b98a5` | The zoom level that `<Controls>` shows. |
|
|
456
|
+
| `border` | `#4d5966` | The border of a node. |
|
|
457
|
+
| `selected` | `#67d9bc` | A selected node or edge. |
|
|
458
|
+
| `connect` | `#f2cc60` | The border of the node proposed as the target of a connection. |
|
|
459
|
+
| `group` | `#3b4450` | The frame of a `group` node. |
|
|
460
|
+
| `edge` | `#6e7c8a` | An edge. |
|
|
461
|
+
| `animated` | `#79c0ff` | An animated edge. |
|
|
462
|
+
| `label` | `#c9d1d9` | The label of an edge. |
|
|
463
|
+
| `labelBg` | `#1f262e` | The background of an edge label and of the buttons of `<Controls>`. |
|
|
464
|
+
| `marker` | `#8b98a5` | The arrows at the ends of edges. |
|
|
465
|
+
| `background` | `#2d353e` | The marks of `<Background>`. |
|
|
466
|
+
| `handle` | `#f2cc60` | Handles, and the line of a connection being made. |
|
|
467
|
+
| `panelBg` | `#161b22` | The background of the built-in nodes, `<Controls>` and `<MiniMap>`. |
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
import { defaultTheme, type FlowTheme } from "@luciole-sh/flow-graph";
|
|
471
|
+
|
|
472
|
+
export const light: FlowTheme = {
|
|
473
|
+
...defaultTheme,
|
|
474
|
+
text: "#1f2328",
|
|
475
|
+
labelBg: "#f6f8fa",
|
|
476
|
+
panelBg: "#ffffff",
|
|
477
|
+
};
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## Hooks and helpers
|
|
481
|
+
|
|
482
|
+
### `useNodesState` and `useEdgesState`
|
|
483
|
+
|
|
484
|
+
`useNodesState(initial)` returns `[nodes, setNodes, onNodesChange]`. It is `useState`, plus the
|
|
485
|
+
callback that applies the canvas's changes. `useEdgesState(initial)` does the same for edges.
|
|
486
|
+
The program at the top of this page uses both.
|
|
487
|
+
|
|
488
|
+
### `useFlow()` and `FlowInstance`
|
|
489
|
+
|
|
490
|
+
`useFlow()` returns the `FlowInstance` of the canvas around it, inside `<Flow>` or
|
|
491
|
+
`<FlowProvider>`. Elsewhere it throws
|
|
492
|
+
`useFlow and the canvas components need a <Flow> or <FlowProvider>`.
|
|
493
|
+
|
|
494
|
+
| Member | What it does |
|
|
495
|
+
| ---------------------------------------- | ------------------------------------------------------------------------- |
|
|
496
|
+
| `getNodes()`, `getEdges()` | The nodes and edges the canvas last received. |
|
|
497
|
+
| `getNode(id)` | One node, or `undefined`. |
|
|
498
|
+
| `getViewport()`, `setViewport(viewport)` | Read or set the view. |
|
|
499
|
+
| `fitView()` | The closest zoom level at which every node is seen, centred. |
|
|
500
|
+
| `zoomIn()`, `zoomOut()` | One zoom level in or out, around the centre. |
|
|
501
|
+
| `setCenter(point, zoom?)` | Centres the view on a flow point, at `zoom` or the current level. |
|
|
502
|
+
| `reveal(id)` | Pans just enough to show node `id` whole. |
|
|
503
|
+
| `select({ nodes, edges })` | Proposes selecting exactly these ids, through the change callbacks. |
|
|
504
|
+
| `deleteElements({ nodes, edges })` | Proposes removing these ids. A node takes its edges and children with it. |
|
|
505
|
+
| `center()` | The flow point at the centre of the view. |
|
|
506
|
+
|
|
507
|
+
```tsx
|
|
508
|
+
import { useFlow, type FlowInstance } from "@luciole-sh/flow-graph";
|
|
509
|
+
|
|
510
|
+
export function FitButton() {
|
|
511
|
+
const flow: FlowInstance = useFlow();
|
|
512
|
+
return <text onMouseDown={() => flow.fitView()}>fit</text>;
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
`useViewport()` returns the current `{ x, y, zoom, detail }` and re-renders when it changes. It
|
|
517
|
+
throws the same error outside a canvas.
|
|
518
|
+
|
|
519
|
+
### `addEdge` and `getEdgeId`
|
|
520
|
+
|
|
521
|
+
`addEdge(connection, edges, getId?)` returns a new array with an edge for `connection`. The
|
|
522
|
+
array is unchanged when an edge joins the same nodes and handles, or when `source` or `target`
|
|
523
|
+
is empty. The new edge keeps the connection's `id`, or takes `getId(connection)`.
|
|
524
|
+
|
|
525
|
+
`getEdgeId(connection)` is the default `getId`. It returns
|
|
526
|
+
`xy-edge__<source><sourceHandle>-<target><targetHandle>`, with an empty string for a `null`
|
|
527
|
+
handle.
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
import { addEdge, getEdgeId, type Edge } from "@luciole-sh/flow-graph";
|
|
531
|
+
|
|
532
|
+
const connection = { source: "lint", target: "build", sourceHandle: null, targetHandle: null };
|
|
533
|
+
getEdgeId(connection); // "xy-edge__lint-build"
|
|
534
|
+
export const edges: Edge[] = addEdge({ ...connection, animated: true }, []);
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### `applyNodeChanges` and `applyEdgeChanges`
|
|
538
|
+
|
|
539
|
+
`applyNodeChanges(changes, nodes)` returns a new array. A node without a change keeps its
|
|
540
|
+
identity, and a changed node is a copy. `applyEdgeChanges(changes, edges)` does the same for
|
|
541
|
+
edges. Neither raises an error. A change for an unknown id is ignored. See
|
|
542
|
+
[Apply the canvas's changes](#apply-the-canvass-changes).
|
|
543
|
+
|
|
544
|
+
### `ZOOMS` and `detailFor`
|
|
545
|
+
|
|
546
|
+
`ZOOMS` is `[1, 0.5, 0.25]`, the three zoom levels. `detailFor(zoom)` returns the `Detail` drawn
|
|
547
|
+
at a zoom: `full` from 1, `compact` from 0.5, `dot` below.
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
import { detailFor } from "@luciole-sh/flow-graph";
|
|
551
|
+
|
|
552
|
+
detailFor(0.5); // "compact"
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### `flowBounds` and `fitViewport`
|
|
556
|
+
|
|
557
|
+
`flowBounds(nodes, measured)` returns the `Rect` around the visible nodes, in flow cells, or
|
|
558
|
+
`null` when there is none. `measured` maps node ids to measured sizes. Pass `new Map()` to use
|
|
559
|
+
the sizes of the built-in nodes.
|
|
560
|
+
|
|
561
|
+
`fitViewport(bounds, canvas, padding?)` returns the `Viewport` that centres `bounds` in a canvas
|
|
562
|
+
of `{ width, height }` cells. It picks the closest level of `ZOOMS` at which `bounds` fits
|
|
563
|
+
inside `padding` cells, 2 by default. Its zoom is ¼ when nothing fits, and 1 when `bounds` is
|
|
564
|
+
`null`.
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
import { fitViewport, flowBounds, type Node } from "@luciole-sh/flow-graph";
|
|
568
|
+
|
|
569
|
+
const nodes: Node[] = [
|
|
570
|
+
{ id: "checkout", position: { x: 0, y: 3 }, data: { label: "checkout" } },
|
|
571
|
+
{ id: "build", position: { x: 40, y: 3 }, data: { label: "build" } },
|
|
572
|
+
];
|
|
573
|
+
const bounds = flowBounds(nodes, new Map()); // { x: 0, y: 3, width: 49, height: 3 }
|
|
574
|
+
fitViewport(bounds, { width: 80, height: 24 }); // { x: 16, y: 8, zoom: 1 }
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### `composeFrame`, `hitTest`, `Grid` and `Frame`
|
|
578
|
+
|
|
579
|
+
The frame under the nodes, as pure functions, for tests and for rendering off the canvas.
|
|
580
|
+
|
|
581
|
+
- `composeFrame(input)` returns a `Frame`: `{ grid, placed, anchors }`. It draws the edges,
|
|
582
|
+
their labels and the background. It takes `size`, `viewport`, `nodes`, `edges` and
|
|
583
|
+
`measured`, plus the optional `handles`, `background`, `connecting`, `edgeTypes`,
|
|
584
|
+
`defaultEdgeType`, `braille` and `phase`.
|
|
585
|
+
- `handles` maps a node id to its `HandleSpec`s, each `{ id, type, position }` as a `<Handle>`
|
|
586
|
+
declares it.
|
|
587
|
+
- `Grid` holds the cells: `lines()` returns its rows as text, and `get(x, y)` returns one cell.
|
|
588
|
+
- `hitTest(frame, at)` returns what is at a canvas cell. Its `kind` is `"handle"` with the
|
|
589
|
+
`anchor`, `"node"` with the `node`, `"edge"` with the edge's `id`, or `"pane"`.
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
import { composeFrame, hitTest, type Node } from "@luciole-sh/flow-graph";
|
|
593
|
+
|
|
594
|
+
const nodes: Node[] = [
|
|
595
|
+
{ id: "a", position: { x: 0, y: 0 }, data: { label: "a" } },
|
|
596
|
+
{ id: "b", position: { x: 16, y: 0 }, data: { label: "b" } },
|
|
597
|
+
];
|
|
598
|
+
const frame = composeFrame({
|
|
599
|
+
size: { width: 24, height: 3 },
|
|
600
|
+
viewport: { x: 0, y: 0, zoom: 1 },
|
|
601
|
+
nodes,
|
|
602
|
+
edges: [{ id: "a-b", source: "a", target: "b", label: "go" }],
|
|
603
|
+
measured: new Map(),
|
|
604
|
+
});
|
|
605
|
+
frame.grid.lines()[1]; // " ── go ─▶"
|
|
606
|
+
hitTest(frame, { x: 10, y: 1 }); // { kind: "edge", id: "a-b" }
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## What differs from React Flow
|
|
610
|
+
|
|
611
|
+
- **Cell coordinates.** A row is about twice as tall as a column is wide.
|
|
612
|
+
- **Semantic zoom.** `ZOOMS` has three levels: 1, ½ and ¼. Zooming out moves nodes closer and
|
|
613
|
+
draws less of each: the node's component (`full`), its label on one row (`compact`), then a
|
|
614
|
+
dot in `node.color` (`dot`).
|
|
615
|
+
- **Edges on the grid.** `smoothstep` (the default) and `step` use box characters, and the
|
|
616
|
+
lines of every edge in a cell merge into `├ ┤ ┬ ┴ ┼`. `bezier` and slanted `straight` edges use
|
|
617
|
+
braille. `animated` edges draw dashes that move toward the target.
|
|
618
|
+
- **Edge types.** An `edgeTypes` entry is a built-in style, or a function that returns the
|
|
619
|
+
corners of the path (`EdgeRoute`). There is no free render component, as SVG gives React Flow.
|
|
620
|
+
- **Node types.** `default` has a rounded border, `input` a double one, `output` a thick one,
|
|
621
|
+
and `group` a frame of fixed size whose children set `parentId`. Without a `<Handle>`, a node
|
|
622
|
+
takes edges on its left and sends them from its right.
|
|
623
|
+
- **Text in custom nodes.** Set `selectable={false}` on it. Otherwise a click starts a text
|
|
624
|
+
selection instead of a drag.
|
|
625
|
+
|
|
626
|
+
## Keyboard and mouse
|
|
627
|
+
|
|
628
|
+
The keys belong to the group `flow`. Set `keyboard={false}` to turn them off. They run
|
|
629
|
+
through `@opentui/keymap`: inside an app with its own `<KeymapProvider>`, such as a luciole app,
|
|
630
|
+
they join its layers, and `<KeyHelp>` lists them. Elsewhere the canvas installs its own keymap.
|
|
631
|
+
|
|
632
|
+
| Keys | Action |
|
|
633
|
+
| ---------------------------------- | ---------------------------------------------------------------- |
|
|
634
|
+
| `h j k l`, arrows | Move the view |
|
|
635
|
+
| `H J K L`, <kbd>Shift</kbd>+arrows | Move the selected nodes by one cell |
|
|
636
|
+
| `tab`, <kbd>Shift</kbd>+`tab` | Next, previous node, by column from left to right |
|
|
637
|
+
| `]`, `[` | Follow an edge downstream, upstream |
|
|
638
|
+
| `}`, `{` | Next, previous sibling node |
|
|
639
|
+
| `c`, then `tab` or `]`, `Enter` | Connect the selected node to the proposed target (`Esc` cancels) |
|
|
640
|
+
| `e` | Select the node's edges in turn |
|
|
641
|
+
| `x`, `Delete` | Delete the selection. A node takes its edges with it |
|
|
642
|
+
| `=` or `+`, `-`, `0` | Zoom in, zoom out, fit everything |
|
|
643
|
+
| `Esc` | Clear the selection |
|
|
644
|
+
|
|
645
|
+
With the mouse:
|
|
646
|
+
|
|
647
|
+
- A click selects. <kbd>Shift</kbd>+click adds to the selection.
|
|
648
|
+
- Dragging a node moves it, with the other selected nodes.
|
|
649
|
+
- Dragging the background moves the view.
|
|
650
|
+
- Dragging from a handle (`●`, shown on the selected node) to a node connects them.
|
|
651
|
+
- The wheel moves the view, horizontally with <kbd>Shift</kbd>. <kbd>Ctrl</kbd> or <kbd>Alt</kbd> and the wheel zoom.
|
|
652
|
+
- A click in the minimap centres the view there.
|
|
653
|
+
|
|
654
|
+
## Limits
|
|
655
|
+
|
|
656
|
+
- The package is 0.x. Until 1.0, a minor release may break the API.
|
|
657
|
+
- It is tested on Bun only. The `bun` export condition points to the TypeScript sources.
|
|
658
|
+
- Zoom has three fixed levels, because a terminal cannot scale glyphs.
|
|
659
|
+
- Curves need braille glyphs in your terminal's font. Set `braille={false}` where they are missing.
|
|
660
|
+
- A `group` node needs a `width` and a `height`.
|
|
661
|
+
- Edges are drawn in box characters. They cannot be arbitrary components.
|
|
662
|
+
- It does not lay nodes out for you. You give every node a `position`.
|
|
663
|
+
|
|
664
|
+
## Origin of the code
|
|
665
|
+
|
|
666
|
+
The orthogonal routing of edges, the bezier control points and the application of changes come
|
|
667
|
+
from xyflow (MIT), in [`src/vendor/xyflow`](https://github.com/sykar-f/luciole/blob/v0.1.0/packages/flow-graph/src/vendor/xyflow/README.md). Rendering and
|
|
668
|
+
interaction are the package's own.
|
|
669
|
+
|
|
670
|
+
## Licence
|
|
671
|
+
|
|
672
|
+
MIT.
|