Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/flow-vertical-orientation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/kumo": minor
---

Add `orientation="vertical"` support to `Flow` for laying out nodes top-to-bottom. The `align` prop now controls cross-axis alignment in both orientations.
26 changes: 26 additions & 0 deletions packages/kumo-docs-astro/src/components/demos/FlowDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@ export function FlowBasicDemo() {
);
}

/** Vertical flow diagram with sequential nodes */
export function FlowVerticalDemo() {
return (
<Flow orientation="vertical">
<Flow.Node>Step 1</Flow.Node>
<Flow.Node>Step 2</Flow.Node>
<Flow.Node>Step 3</Flow.Node>
</Flow>
);
}

/** Vertical flow diagram with parallel branching */
export function FlowVerticalParallelDemo() {
return (
<Flow orientation="vertical" align="center">
<Flow.Node>Start</Flow.Node>
<Flow.Parallel>
<Flow.Node>Branch A</Flow.Node>
<Flow.Node>Branch B</Flow.Node>
<Flow.Node>Branch C</Flow.Node>
</Flow.Parallel>
<Flow.Node>End</Flow.Node>
</Flow>
);
}

/** Flow diagram with parallel branching */
export function FlowParallelDemo() {
return (
Expand Down
35 changes: 33 additions & 2 deletions packages/kumo-docs-astro/src/pages/components/flow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import ComponentExample from "~/components/docs/ComponentExample.astro";
import ComponentSection from "~/components/docs/ComponentSection.astro";
import {
FlowBasicDemo,
FlowVerticalDemo,
FlowVerticalParallelDemo,
FlowParallelDemo,
FlowCustomContentDemo,
FlowComplexDemo,
Expand Down Expand Up @@ -100,6 +102,25 @@ export default function Example() {
<FlowParallelDemo client:visible />
</ComponentExample>

### Vertical Orientation

<p>
Set `orientation="vertical"` to lay nodes out top-to-bottom instead of the
default left-to-right. Connectors, parallel branches, and `align` all adapt
to the vertical axis.
</p>
<ComponentExample demo="FlowVerticalDemo">
<FlowVerticalDemo client:visible />
</ComponentExample>

<p>
Parallel branches work the same way in vertical flows. In this orientation
`align` controls horizontal alignment of nodes.
</p>
<ComponentExample demo="FlowVerticalParallelDemo">
<FlowVerticalParallelDemo client:visible />
</ComponentExample>

### Custom Node Styling

<p>
Expand Down Expand Up @@ -238,12 +259,22 @@ export default function Example() {
</tr>
</thead>
<tbody>
<tr class="border-b border-kumo-hairline">
<td class="px-4 py-3 font-mono">orientation</td>
<td class="px-4 py-3 font-mono">"horizontal" | "vertical"</td>
<td class="px-4 py-3">
Layout direction of the flow. `"horizontal"` (default) lays nodes out
left-to-right, `"vertical"` lays them out top-to-bottom.
</td>
</tr>
<tr class="border-b border-kumo-hairline">
<td class="px-4 py-3 font-mono">align</td>
<td class="px-4 py-3 font-mono">"start" | "center"</td>
<td class="px-4 py-3">
Vertical alignment of nodes. `"start"` (default) aligns to top,
`"center"` vertically centers nodes.
Cross-axis alignment of nodes. In horizontal orientation this is
vertical alignment; in vertical orientation it is horizontal
alignment. `"start"` (default) aligns to the start of the cross axis,
`"center"` centers nodes.
</td>
</tr>
<tr class="border-b border-kumo-hairline">
Expand Down
24 changes: 24 additions & 0 deletions packages/kumo-docs-astro/src/pages/tests/flow.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import BaseLayout from "../../layouts/BaseLayout.astro";
import {
FlowBasicDemo,
FlowVerticalDemo,
FlowVerticalParallelDemo,
FlowParallelDemo,
FlowCustomContentDemo,
FlowComplexDemo,
Expand All @@ -28,6 +30,28 @@ import {
</div>
</section>

<section class="space-y-4">
<h2 class="text-xl font-semibold text-kumo-default">FlowVerticalDemo</h2>
<p class="text-kumo-subtle">
Vertical flow diagram with sequential nodes
</p>
<div class="p-4 rounded-lg border border-kumo-hairline bg-kumo-base">
<FlowVerticalDemo client:visible />
</div>
</section>

<section class="space-y-4">
<h2 class="text-xl font-semibold text-kumo-default">
FlowVerticalParallelDemo
</h2>
<p class="text-kumo-subtle">
Vertical flow diagram with parallel branching
</p>
<div class="p-4 rounded-lg border border-kumo-hairline bg-kumo-base">
<FlowVerticalParallelDemo client:visible />
</div>
</section>

<section class="space-y-4">
<h2 class="text-xl font-semibold text-kumo-default">FlowParallelDemo</h2>
<p class="text-kumo-subtle">Flow diagram with parallel branching</p>
Expand Down
45 changes: 31 additions & 14 deletions packages/kumo/src/components/flow/diagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ function isEventFromNode(target: EventTarget | null): boolean {
/** Minimum scrollbar thumb size in percentage to ensure visibility */
const MIN_SCROLLBAR_THUMB_SIZE = 10;

// Vertical orientation is currently a no-op
type Orientation = "horizontal" | "vertical";
type Align = "start" | "center";

Expand All @@ -66,9 +65,11 @@ export function useDiagramContext(): DiagramContextValue {
interface FlowDiagramProps {
orientation?: Orientation;
/**
* Controls vertical alignment of nodes in horizontal orientation.
* - `start`: Nodes align to the top (default)
* - `center`: Nodes are vertically centered
* Controls cross-axis alignment of nodes.
* In horizontal orientation this is vertical alignment; in vertical
* orientation it is horizontal alignment.
* - `start`: Nodes align to the start of the cross axis (default)
* - `center`: Nodes are centered on the cross axis
*/
align?: Align;
/**
Expand Down Expand Up @@ -428,11 +429,26 @@ export function FlowNodeList({ children }: { children: ReactNode }) {
if (currentRect && nextRect) {
const isDisabled =
currentNode.props.disabled || nextNode.props.disabled;
// Horizontal flows left-to-right: connect the right edge of the
// current node to the left edge of the next. Vertical flows
// top-to-bottom: connect the bottom edge of the current node to the
// top edge of the next, using horizontal centers.
const edge =
orientation === "vertical"
? {
x1: currentRect.left - offsetX + currentRect.width / 2,
y1: currentRect.top - offsetY + currentRect.height,
x2: nextRect.left - offsetX + nextRect.width / 2,
y2: nextRect.top - offsetY,
}
: {
x1: currentRect.left - offsetX + currentRect.width,
y1: currentRect.top - offsetY + currentRect.height / 2,
x2: nextRect.left - offsetX,
y2: nextRect.top - offsetY + nextRect.height / 2,
};
edges.push({
x1: currentRect.left - offsetX + currentRect.width,
y1: currentRect.top - offsetY + currentRect.height / 2,
x2: nextRect.left - offsetX,
y2: nextRect.top - offsetY + nextRect.height / 2,
...edge,
disabled: isDisabled,
single: true,
fromId: currentNode.id,
Expand All @@ -442,7 +458,7 @@ export function FlowNodeList({ children }: { children: ReactNode }) {
}

setConnectors(edges);
}, [descendants.descendants]);
}, [descendants.descendants, orientation]);

/**
* Recompute connectors after layout so that containerRect and node rects are
Expand Down Expand Up @@ -497,12 +513,13 @@ export function FlowNodeList({ children }: { children: ReactNode }) {
<div className="relative" ref={containerRef}>
<ul
className={cn(
"ml-0 list-none",
"ml-0 list-none gap-16",
orientation === "vertical"
? "grid auto-rows-min gap-16"
: "flex gap-16",
orientation === "horizontal" &&
(align === "center" ? "items-center" : "items-start"),
? cn(
"flex flex-col w-fit",
align === "center" ? "items-center" : "items-start",
)
: cn("flex", align === "center" ? "items-center" : "items-start"),
)}
>
{children}
Expand Down
25 changes: 25 additions & 0 deletions packages/kumo/src/components/flow/flow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,31 @@ describe("Flow", () => {
expect(path).toBe("M 0 17 L 32 17 L 32 63 Q 32 71 40 71 L 48 71");
expect(path).not.toContain(",");
});

it("routes vertical paths through a horizontal mid-segment", () => {
const path = createRoundedPath(
{ x1: 17, y1: 0, x2: 71, y2: 56 },
{ orientation: "vertical", single: false },
);

expect(path).toContain("M 17 0");
expect(path).not.toContain(",");
});
});

describe("Vertical orientation", () => {
it("renders sequential nodes top-to-bottom in a column", () => {
render(
<Flow orientation="vertical">
<Flow.Node>Step 1</Flow.Node>
<Flow.Node>Step 2</Flow.Node>
<Flow.Node>Step 3</Flow.Node>
</Flow>,
);

const list = screen.getByText("Step 1").closest("ul");
expect(list?.className).toContain("flex-col");
});
});

describe("Compound component API", () => {
Expand Down
Loading