# The flow kit — a node-and-edge canvas whose nodes are your own Osy#

> A modelling canvas — pan, zoom, drag nodes, draw edges between ports, select and delete, by mouse and by keyboard — shipped as an optional KIT. Nodes and edges are your app's own rows, and each node is rendered by a template you write in Osy#, so a node can hold anything a page can. It lays a graph out itself and can draw a run's path over it. The dragging is React Flow's framework-free core; the layout is the kit's own.

<!-- id: ui-flow-kit · area: ui · stability: preview · html: https://osysharp.com/reference/ui/flow-kit/ -->

## Summary        {#summary}

`Osysharp.Diagrams` is a canvas for things that are **boxes joined by arrows**: the steps of a process, the stages of a
pipeline, an approval chain, an org chart, a live picture of a workflow. You depend on it in one line:

```osy syntax
app Designer {
  use Osysharp.Ui;
  use Osysharp.Diagrams@0;
  model "model/**/*.osy";
}
```

Three things make it different from a diagramming widget:

- **Nodes and edges are your rows.** The canvas reads them; a drag or a new edge comes back as an **event carrying the
  row**, and your action writes it through the unit of work like any other edit — so it is committed, undone
  (`UnitOfWork.Discard()`), secured and synced exactly as the rest of your app.
- **A node is your own Osy#.** You write one template per node *type* (`slot Task { s => … }`); the platform renders
  it into the node, so a node can show a field, a badge, a button — anything a page can — and it stays live.
- **It works without a mouse.** Tab to a node, move it with the arrow keys, press Enter on one port and Enter on
  another to connect them, Delete to remove.

⚠️ **Preview (0.x).** Everything below is built and proven in a real browser, but the surface may still change before
1.0.

**Every `…Field` prop is a typed member selector** — `xField: n => n.X`, `labelField: n => n.Title` — checked against
your row type when the page compiles. A misspelt member is a compile error naming the member and the type (with a
did-you-mean); a member of the wrong type, a path (`n => n.Owner.Name`) or a computed value is refused too, because
the canvas reads that one member off each row itself. A string (`xField: "X"`) is refused with the selector to write.

## Signature      {#signature}

```osy syntax
FlowCanvas(
  nodes: steps,                 // your rows — positioned by xField/yField (default n => n.X / n => n.Y)
  edges: links,                 // your rows — joined by sourceField/targetField (default e => e.Source / e => e.Target)
  nodeTypes: [ new FlowNodeType { Name = "Task", Ports = [
                 new FlowPort { Id = "in",  Kind = "target", Side = "left" },
                 new FlowPort { Id = "out", Kind = "source", Side = "right" } ] } ],
  typeField: n => n.Kind,            // which field names each node's type (omitted: every node is the first type)
  sourcePortField: e => e.SourcePort, targetPortField: e => e.TargetPort,   // which fields name an edge's ports
  labelField: n => n.Title,          // a node's accessible name (omitted: the words its template rendered)
  edgeLabelField: e => e.Label,      // drawn on the edge and read in its name
  readOnly: false,              // true: a picture — pan and zoom only
  edgeShape: bezier,            // bezier | smoothstep | straight — an editor's edges; a picture routes its own
  snap: 20,                     // drops and arrow keys land on a 20-unit grid (0 = free)
  background: dots,             // dots | lines | none — the grid pans and zooms with the canvas
  controls: true,               // zoom in / zoom out / fit buttons
  minimap: true,                // an overview in the corner; drag it to pan
  autoLayout: initial,          // initial | always | none — see "Laying a graph out"
  layoutDirection: auto,        // auto | right | down | left | up
  initialField: n => n.IsInitial, terminalField: n => n.IsTerminal,   // which node fields mark a START and an END
  mainPath: [ … ],              // node keys of the path to keep on one straight row (omitted: worked out)
  nodeSizing: equal,            // equal | natural — a picture's nodes at one size, or each at its own
  tools: [ … ],                 // FlowTool { Label, Run } — the app's own buttons in the corner toolbar
  highlighted: [ … ],           // node keys that are CURRENT (a run's state) — see "A live picture of a workflow run"
  visited: [ … ],               // node keys a run has passed through, in order
  path: [ … ],                  // the exact way it went, repeats included, when you know it
  failed: [ … ],                // node keys where it failed
  gesture: Gesture,             // (string label) — ONCE before each edit's events; mark the unit of work here
  nodeMoved: Move,              // (TNode node, double x, double y)
  connected: Link,              // (TNode source, TNode target, string sourcePort, string targetPort)
  edgeRemoved: Unlink,          // (TEdge edge)
  nodeRemoved: Remove,          // (TNode node)
  nodeSelected: Pick,           // (TNode node) — the primary selection
  selectionChanged: Selected    // (TNode[] nodes, TEdge[] edges) — the whole selection, as rows
) {
  slot Task { s => … }          // one template per node type — the names are checked against nodeTypes
}
```

The canvas has no size of its own: it declares `sizing fill`, so the platform makes it fill the box you put it in —
give that box a height ([sizing — how the platform sizes a control's box](https://osysharp.com/reference/ui/control-sizing/)).

## Description    {#description}

### The whole thing, at a call site   {#app-side}

```osy title="a process designer — rows in, edits out" test app=ui-flow-kit
// ⚠ This example DECLARES the control inline, because a documentation example compiles on its own with no manifest
// to carry a `use`. In your app you write `use Osysharp.Diagrams@0;` and `using Osysharp.Diagrams;` instead and delete this
// block — the declaration arrives with the kit. What follows is `kits/diagrams/flow.osy`, trimmed to what this page uses.
public class FlowPort { public string Id; public string Kind; public string Side; }
public class FlowNodeType { public string Name; public FlowPort[] Ports; }

control FlowCanvas<TNode, TEdge> {
  contractVersion "1.1"
  participation headless
  sizing fill
  props {
    TNode[] nodes;
    TEdge[] edges;
    FlowNodeType[] nodeTypes;
    Func<TNode, string>? typeField;
    Func<TNode, double> xField = n => n.X;
    Func<TNode, double> yField = n => n.Y;
    Func<TEdge, object> sourceField = e => e.Source;
    Func<TEdge, object> targetField = e => e.Target;
    bool readOnly = false;
  }
  events {
    nodeMoved(TNode node, double x, double y);
    connected(TNode source, TNode target, string sourcePort, string targetPort);
    edgeRemoved(TEdge edge);
  }
  slots from nodeTypes.Name
  chunks { XY; }
}

[Principal] entity Planner { [Required] string Email; }

entity Step {
  [MaxLength(80)] string Title;
  double X;
  double Y;
  security { allow read, create, update, delete when IsAuthenticated; }
}

entity Link {
  Step Source;
  Step Target;
  security { allow read, create, update, delete when IsAuthenticated; }
}

[Route("/process")]
component Process() {
  live var steps = Step.ToList();
  live var links = Link.ToList();

  // A drop is one event, carrying the ROW — never one per frame.
  action Move(Step s, double x, double y) { s.X = x; s.Y = y; UnitOfWork.Commit(); }
  action Connect(Step from, Step to, string fromPort, string toPort) {
    new Link { Source = from, Target = to };
    UnitOfWork.Commit();
  }
  action Unlink(Link l) { l.Delete(); UnitOfWork.Commit(); }

  render {
    Box(h: "480px") {
      FlowCanvas(
        nodes: steps,
        edges: links,
        nodeTypes: [ new FlowNodeType { Name = "Task", Ports = [
          new FlowPort { Id = "in", Kind = "target", Side = "left" },
          new FlowPort { Id = "out", Kind = "source", Side = "right" } ] } ],
        nodeMoved: Move,
        connected: Connect,
        edgeRemoved: Unlink
      ) {
        slot Task { s => Text(s.Title); }
      }
    }
  }
}
```

### Why a move is an event and not a binding   {#events}

The canvas never writes your data. It moves the node on screen at once (no round trip while the pointer is down), and
on the **drop** raises `nodeMoved(row, x, y)` — once. Your action decides what that means: store it, snap it,
refuse it. The same holds for `connected`: a new edge is a row *you* create, so your model can carry whatever an edge
means to it (a label, a condition, a rule) and your `security { }` governs who may draw one.

### Undo, one gesture at a time   {#undo}

Because every edit is an ordinary write through your unit of work, undo is the unit of work's: `UnitOfWork.Discard()`
throws a whole draft away, and its SAVEPOINTS ([UnitOfWork savepoints — Mark, Undo, Redo, RollbackTo](https://osysharp.com/reference/function/unit-of-work-savepoints/)) take it back step by step. The
canvas raises `gesture(label)` **once, before** the events of each edit — a drag, an arrow-key press, a new connection,
a Delete, a lay-out — with a sentence naming it ("Move Approve?", "Connect Receive request to Fulfil", "Remove 2 nodes
and 1 edge"). Mark there, and one Ctrl+Z takes back one whole gesture, however many rows it moved:

```osy title="step-by-step undo on a draft canvas" syntax
action Gesture(string what) { UnitOfWork.Mark(what); }
action Undo() { UnitOfWork.Undo(); }
action Redo() { UnitOfWork.Redo(); }
…
Button(UnitOfWork.CanUndo() ? "Undo " + UnitOfWork.UndoLabel() : "Undo", onPress: Undo, shortcut: "Mod+Z",
       disabled: !UnitOfWork.CanUndo());
Button(UnitOfWork.CanRedo() ? "Redo " + UnitOfWork.RedoLabel() : "Redo", onPress: Redo, shortcut: "Mod+Shift+Z",
       disabled: !UnitOfWork.CanRedo());
FlowCanvas(nodes: steps, edges: links, gesture: Gesture, nodeMoved: Moved, connected: Link, edgeRemoved: Unlink,
           nodeRemoved: Remove) { … }
```

The shortcuts work with focus on the canvas — it lets them through — and the canvas redraws whatever the rows are
after an undo, so it keeps no history of its own. A Save (`UnitOfWork.Commit()`) clears the marks. `demo/flow-demo`'s
`/draft` page is this, whole.

### Where edges attach — node types and their ports   {#types}

Each `FlowNodeType` names a type and its **ports** — where edges attach. A port's `Kind` is `source` (edges leave)
or `target` (edges arrive); its `Side` is `left`, `right`, `top` or `bottom`, and several ports on one side are
spread along it. `slots from nodeTypes.Name` makes each type's name a template you write at the call site, and a
misspelt `slot Tsak` is a compile error naming the types you passed. A single anonymous template (`{ s => … }`)
renders every type the same way.

The canvas draws the ports itself, on the node's border — your template never has to place them.

### Selecting many   {#selection}

Click selects one node; **Shift**, **Ctrl** or **Cmd**-click adds or removes one; **Shift + drag** on the empty pane
draws a box and selects every node inside it and the edges between them; **Ctrl/Cmd + A** selects every node; from the
keyboard, **Space** on a node adds or removes it. A drag, an arrow key or **Delete** on a node that is in the selection
acts on the whole selection — each node still arrives as its own `nodeMoved` / `nodeRemoved` (edges first, so an app
whose node delete cascades its edges never hears about an edge that is already gone). **Escape** clears it.

`selectionChanged(TNode[] nodes, TEdge[] edges)` hands your action the selection as **rows** — live entities, not
copies — so it can read and write them like any others:

```osy syntax
action Selected(Step[] steps, Transition[] links) {
  selection = string.Join(", ", steps.Select(s => s.Title));
}
```

### Grid, snapping and the view   {#view}

`snap: 20` puts every drop on a 20-unit grid and makes an arrow press one grid step. The background grid (`dots`,
`lines` or `none`) follows the snap and pans and zooms with the canvas, so what you see is where a node will land.
The **controls** in the corner zoom and fit the whole picture (the keys are **+**, **−** and **0**); every view change
glides over the theme's `Motion.Normal`, and not at all for someone who prefers reduced motion. `minimap: true` adds an
overview that marks the current view; drag it to move around a large diagram.

### Laying a graph out   {#layout}

The canvas can place the nodes itself — in ranks from the starts to the ends, edges flowing one way, the main path on
one straight row, nothing overlapping. The layout is the kit's own and its own chunk (`Layout`, ~15 KB), downloaded the
first time a canvas lays itself out; a canvas that only draws stored positions never fetches it.

| `autoLayout` | when the canvas lays out |
|---|---|
| `initial` *(default)* | once, for a graph nobody has placed yet (every node at the same point) |
| `always` | every time the nodes or edges change, ignoring the position fields — a **picture** of a structure |
| `none` | never on its own |

`layoutDirection` is `auto` (default — along the page's reading direction, or top to bottom when that shows the
picture larger), `right`, `down`, `left` or `up`. The **Tidy** button in the corner (and **Shift + L**) lays out on
demand. In an editor, every node that moves raises `nodeMoved`, so a layout is a batch of ordinary edits: the app
stores it, and can discard it like any other. Nodes glide to their places over the theme's motion.

With `always` the canvas draws a **picture**: it also routes every edge on a line of its own, keeps the whole of it
fitted to its box, and holds its nodes to one size. How the layout works, what it promises, and how a test asserts it
are on their own page: [Laying a graph out — ranks, routed edges and a run's path on the flow canvas](https://osysharp.com/reference/ui/flow-layout/).

A layout — or any other verb — can also be one of the app's own **tools**, the text buttons it adds to that corner:

```osy syntax
FlowCanvas(…, tools: [
  new FlowTool { Label = "Lay out", Run = FlowCanvas.Layout },   // one of the canvas's commands
  new FlowTool { Label = "Save", Run = Save }                     // one of yours
])
```

The canvas's commands are `Layout` and `FitView`.

### A live picture of a workflow run   {#workflow-picture}

Any `workflow` can be drawn, with a run's place in it, from three ordinary reads — nothing in the canvas knows what a
workflow is:

```osy syntax
component OrderRun(Order order) {
  live var times = Fulfilment.For(order).TimeInStates;     // where this run has BEEN (a server read, live)

  render {
    FlowCanvas(
      nodes: Fulfilment.Graph.States,                      // every state — built by the compiler, no server read
      edges: Fulfilment.Graph.Transitions,                 // every declared transition
      sourceField: e => e.From, targetField: e => e.To,
      labelField: n => n.Label, edgeLabelField: e => e.Trigger,
      readOnly: true,
      autoLayout: always,                                  // states have no positions: the canvas lays them out
      highlighted: [order.Status.ToString()],              // where it IS — the row's own tracked field, so live
      visited: times.Select(t => t.State).ToList()
    ) { s => Text(s.Label); }
  }
}
```

`highlighted` marks the current node — a ring that breathes (still, for someone who prefers reduced motion), *"Current"*
to a screen reader — and `visited` marks the ones the run passed through, with a tick. The edges between one visited
node and the next are drawn as **taken** — heavier, in the accent colour — and the one that brought the run to where it
is now flows; everything the run has not reached is drawn quietly. `failed` marks where a run failed, and `path` gives
the exact way it went when it passed a node more than once ([[ui-flow-layout#run]]). When the run moves, the row
changes, so the ring moves with it and the move is announced (*"Now at Packed."*). See [Graph — every state and every transition of a workflow](https://osysharp.com/reference/workflow/graph/) for what the
graph holds.

Rows here are **values** (`WorkflowStateInfo`), not entities: the canvas keys a row by its `id`, or by an `Id` field
when it has none — so any list of the app's own class values can be drawn, not only table rows.

A state's `Outcome` (empty, or `Success`/`Cancel`/`Error`) makes a good `typeField`: give the terminal types only an
incoming port, and the picture shows that nothing leaves them.

### Read-only — a picture of a process   {#read-only}

`readOnly: true` turns the canvas into a view: nothing moves, nothing connects, nothing is deleted, and pan and zoom
still work. It is the shape for showing where a running workflow is, beside the designer that edits it.

### Using it without a mouse, and with a screen reader   {#keyboard}

The canvas is fully usable without a pointer, and everything it does is said to a screen reader.

| key | on | does |
|---|---|---|
| Tab | the canvas | moves between nodes and their ports — **not** edges (see E) |
| ← → ↑ ↓ | a node | moves it 10 units (50 with Shift) and raises `nodeMoved` |
| Alt + ← → ↑ ↓ | a node | goes to the nearest node in that direction |
| E / Shift + E | a node or an edge | goes to the node's next / previous edge (outgoing first) |
| S / T | an edge | goes to the edge's source / target node |
| Escape | an edge, a node | backs out: to the node the edge was reached from, then to the canvas; cancels a pending connection |
| Enter / Space | a source port, then a target port | draws an edge (`connected`) |
| Delete / Backspace | a node or an edge | raises `nodeRemoved` / `edgeRemoved`; focus moves to a neighbour, never lost |
| Space | a node | adds it to the selection or takes it out |
| Ctrl/Cmd + A | the canvas | selects every node |
| Shift + L | the canvas | tidies the layout (not in a read-only picture) |
| + / − / 0 | the canvas | zoom in / zoom out / fit the picture |
| ? | the canvas | reads these keys aloud |

Edges are out of the Tab order on purpose: a graph has more edges than nodes, and the question a person asks is
"what is *this* connected to?", which is E on the node.

**Names.** Give the canvas `labelField: n => n.Title` and each node is announced by that field; without it, the words its
template rendered are used (buttons included), so name one when you can. `edgeLabelField` draws a label on each edge
and reads it as part of the edge's name — *"Edge from Approve? to Fulfil, yes"*. A node is also described by its type
and how it is connected (*"Decision. 1 edge out, 1 in."*), and moves, connections and removals are announced.

Keys typed *inside* a node's own content (an input in your template) are left alone.

### What it is built on   {#built-on}

The gestures and geometry are `@xyflow/system` — the framework-independent core of React Flow and Svelte Flow, MIT —
with d3-zoom and d3-drag (ISC). They ship as one chunk (`XY`, ~84 KB), loaded when a canvas first mounts. The layout
and the edge routing are the kit's own code, a second chunk (`Layout`, ~15 KB) loaded only when used. The kit contains
no React.

## See also       {#see-also}
- [Laying a graph out — ranks, routed edges and a run's path on the flow canvas](https://osysharp.com/reference/ui/flow-layout/) — how the canvas lays a graph out, routes its edges and draws a run's path
- [control — foreign UI controls (charts, grids, maps)](https://osysharp.com/reference/ui/controls/) — the foreign-control ABI the canvas is built on, including per-row templates and `slots from`
- [chunks — assets a control loads on demand](https://osysharp.com/reference/ui/control-chunks/) — how the `XY` chunk ships and loads
- [Slot(item) — let the caller decide what each row looks like](https://osysharp.com/reference/ui/slot-template/) — the same caller-owns-the-row idea for a component
- [The barcode kit — a QR and barcode scanner you opt into](https://osysharp.com/reference/ui/barcode-kit/) — another optional control kit
- [Graph — every state and every transition of a workflow](https://osysharp.com/reference/workflow/graph/) — `<Wf>.Graph`, a workflow's states and transitions, for a picture of it
- [sizing — how the platform sizes a control's box](https://osysharp.com/reference/ui/control-sizing/) — `sizing fill`, how the canvas takes the box you give it
- [UnitOfWork savepoints — Mark, Undo, Redo, RollbackTo](https://osysharp.com/reference/function/unit-of-work-savepoints/) — Mark / Undo / Redo, what the canvas's step-by-step undo is made of
