Computed flow layout, and mobile written into the design
The canvas lays itself out: a layered graph, left to right on a desktop and top to bottom on a phone, with room reserved for the value each edge carries. Nodes cannot be dragged and `NodeDef.position` is gone from the document — a graph nobody can arrange is one worth keeping small, which is what keeps flows atomic. Endpoints join the same layout, so their lanes and the localStorage that remembered where they were dragged go too. Mobile, per the new Responsive section of DESIGN-GUIDELINES.md: the dock caps its width and wraps instead of running off the screen, the dashboard stacks into one column rather than shrinking a wall panel to a fifth of its size, and Home stops widening its grid track past the viewport. A Playwright project at a phone's width fails the build when a screen no longer fits. Along the way: publish is the checkmark that was already there rather than a button that appears and disappears, with discard beside it on both the flow and the dashboard; the brain reveals a neuron's name on the first tap; and the port sparklines get room to breathe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDSXaRhvqHYNevgDGmNAto
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import dagre from "@dagrejs/dagre"
|
||||
|
||||
/**
|
||||
* Where the nodes of a flow go.
|
||||
*
|
||||
* Nothing on this canvas is placed by hand: a flow is a graph the editor draws,
|
||||
* not a picture someone arranges. That is the design decision — a canvas nobody
|
||||
* can rearrange is one worth keeping small, which is what "atomic flow" means
|
||||
* here — and it also means a flow document carries no positions to go stale.
|
||||
*
|
||||
* Left to right on a desktop, top to bottom on a phone, which is the direction
|
||||
* each screen has room to grow in.
|
||||
*/
|
||||
export type Direction = "LR" | "TB"
|
||||
|
||||
/** `FlowNode` is `min-w-[168px] max-w-[220px]`; an endpoint is narrower. */
|
||||
const NODE_W = 220
|
||||
/** Icon row plus two text lines, as measured. */
|
||||
const NODE_H = 56
|
||||
/**
|
||||
* Room for the live value an edge carries (`LiveEdge`'s chip is
|
||||
* `max-w-[140px]`). Reserved on the edge itself, so dagre routes nodes around
|
||||
* the chip rather than through it.
|
||||
*/
|
||||
const LABEL_W = 150
|
||||
const LABEL_H = 24
|
||||
|
||||
/**
|
||||
* Lay the graph out and return each node's top-left corner.
|
||||
*
|
||||
* ponytail: every node is treated as 220×56 rather than measured. Measuring
|
||||
* would feed the result back into the layout and oscillate; if nodes ever grow
|
||||
* past that box, take the sizes from `node.measured` once they have settled.
|
||||
*/
|
||||
export function layoutGraph(
|
||||
ids: string[],
|
||||
edges: { source: string; target: string }[],
|
||||
direction: Direction,
|
||||
): Map<string, { x: number; y: number }> {
|
||||
const graph = new dagre.graphlib.Graph()
|
||||
graph.setDefaultEdgeLabel(() => ({}))
|
||||
graph.setGraph({
|
||||
rankdir: direction,
|
||||
// Along the rank, and between ranks. A left-to-right graph needs the wider
|
||||
// gap between ranks because the nodes themselves are wide.
|
||||
nodesep: 40,
|
||||
ranksep: direction === "LR" ? 110 : 80,
|
||||
marginx: 40,
|
||||
marginy: 40,
|
||||
})
|
||||
|
||||
// Insertion order is what makes the result deterministic, so it follows the
|
||||
// document rather than whatever order the edges happen to mention nodes in.
|
||||
for (const id of ids) {
|
||||
graph.setNode(id, { width: NODE_W, height: NODE_H })
|
||||
}
|
||||
for (const edge of edges) {
|
||||
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue
|
||||
graph.setEdge(edge.source, edge.target, {
|
||||
width: LABEL_W,
|
||||
height: LABEL_H,
|
||||
labelpos: "c",
|
||||
})
|
||||
}
|
||||
|
||||
dagre.layout(graph)
|
||||
|
||||
// dagre places centres; React Flow wants top-left corners.
|
||||
return new Map(
|
||||
ids.map((id) => {
|
||||
const node = graph.node(id)
|
||||
return [
|
||||
id,
|
||||
node
|
||||
? { x: node.x - NODE_W / 2, y: node.y - NODE_H / 2 }
|
||||
: { x: 0, y: 0 },
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user