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.
This commit is contained in:
@@ -30,69 +30,22 @@ export type EndpointNodeData = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** Lanes either side of the graph, so a label never lands on a node. */
|
||||
const GAP_X = 120
|
||||
const STACK_Y = 64
|
||||
/** Roughly a node's width; only used to find the right-hand lane. */
|
||||
const NODE_W = 220
|
||||
|
||||
export function isEndpointNode(node: { id: string }): boolean {
|
||||
return node.id.startsWith("dashboard:") || node.id.startsWith("flow:")
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the author dragged an endpoint to.
|
||||
* Build the endpoints and wire them to the nodes they touch.
|
||||
*
|
||||
* Not in the flow document — an endpoint is not part of the flow, and writing
|
||||
* a position for one into `flow.json` would be a lie about what it contains.
|
||||
* A view preference belongs to the view, so it lives in the browser.
|
||||
*/
|
||||
const POSITION_KEY = "fluksio-endpoint-positions"
|
||||
|
||||
type Placements = Record<string, Record<string, { x: number; y: number }>>
|
||||
|
||||
function readPlacements(): Placements {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(POSITION_KEY) ?? "{}") as Placements
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function placementsFor(
|
||||
flow: string,
|
||||
): Record<string, { x: number; y: number }> {
|
||||
return readPlacements()[flow] ?? {}
|
||||
}
|
||||
|
||||
export function rememberPlacement(
|
||||
flow: string,
|
||||
id: string,
|
||||
position: { x: number; y: number },
|
||||
): void {
|
||||
const all = readPlacements()
|
||||
all[flow] = { ...(all[flow] ?? {}), [id]: position }
|
||||
try {
|
||||
localStorage.setItem(POSITION_KEY, JSON.stringify(all))
|
||||
} catch {
|
||||
// A full or disabled store just means positions reset; not worth failing.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Place the endpoints and wire them to the nodes they touch.
|
||||
*
|
||||
* Positions are computed rather than stored: an endpoint is not part of the
|
||||
* flow, so there is nowhere to keep a position that would not be a lie about
|
||||
* what the document contains. A producer sits left of what it feeds, a
|
||||
* consumer right of what feeds it.
|
||||
* They carry no position: the caller lays them out together with the flow's
|
||||
* own nodes (see `layout.ts`), so an endpoint that publishes lands upstream of
|
||||
* what it feeds and one that reads lands downstream of what feeds it, by the
|
||||
* same rule that orders everything else.
|
||||
*/
|
||||
export function deriveEndpoints(
|
||||
endpoints: Endpoint[],
|
||||
definitions: NodeDef_Input[],
|
||||
flow: string,
|
||||
positions: Map<string, { x: number; y: number }>,
|
||||
moved: Record<string, { x: number; y: number }> = {},
|
||||
): { nodes: FlowCanvasNode[]; edges: Edge[] } {
|
||||
if (endpoints.length === 0) return { nodes: [], edges: [] }
|
||||
|
||||
@@ -118,38 +71,18 @@ export function deriveEndpoints(
|
||||
}
|
||||
}
|
||||
|
||||
// A lane either side of the graph. Anchoring each label to the node it
|
||||
// feeds put them on top of the nodes, so they live outside the whole thing
|
||||
// instead: producers to the left of everything, consumers to the right.
|
||||
const placed = [...positions.values()]
|
||||
const bounds = {
|
||||
left: placed.length ? Math.min(...placed.map((p) => p.x)) : 0,
|
||||
right: placed.length ? Math.max(...placed.map((p) => p.x)) + NODE_W : 0,
|
||||
top: placed.length ? Math.min(...placed.map((p) => p.y)) : 0,
|
||||
}
|
||||
|
||||
const nodes: FlowCanvasNode[] = []
|
||||
const edges: Edge[] = []
|
||||
// How many labels already sit on each side, so they stack instead of overlap.
|
||||
const stacked = { left: 0, right: 0 }
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
const produces = endpoint.provides ?? []
|
||||
const reads = endpoint.requires ?? []
|
||||
// A producer belongs upstream of what it feeds; everything else downstream.
|
||||
const side = produces.length > 0 ? "left" : "right"
|
||||
|
||||
const index = stacked[side]
|
||||
stacked[side] += 1
|
||||
nodes.push({
|
||||
id: endpoint.id,
|
||||
type: ENDPOINT_TYPE,
|
||||
position: moved[endpoint.id] ?? {
|
||||
x: side === "left" ? bounds.left - GAP_X : bounds.right + GAP_X,
|
||||
y: bounds.top + index * STACK_Y,
|
||||
},
|
||||
// Movable, so a canvas can be arranged; still not the flow's to delete.
|
||||
draggable: true,
|
||||
// Filled in by the layout, along with the flow's own nodes.
|
||||
position: { x: 0, y: 0 },
|
||||
selectable: true,
|
||||
deletable: false,
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user