Skip to content
Open
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
35 changes: 32 additions & 3 deletions packages/kumo-docs-astro/src/components/demos/FlowDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ export function FlowParallelDemo() {
<Flow>
<Flow.Node>Start</Flow.Node>
<Flow.Parallel>
<Flow.Node>Branch A</Flow.Node>
<Flow.List>
<Flow.Node>Branch A1</Flow.Node>
<Flow.Node>Branch A2</Flow.Node>
</Flow.List>
<Flow.Node>Branch B</Flow.Node>
<Flow.Node>Branch C</Flow.Node>
</Flow.Parallel>
Expand All @@ -62,7 +65,9 @@ export function FlowParallelDemo() {
export function FlowCustomContentDemo() {
return (
<Flow>
<Flow.Node render={<li className="rounded-full size-4 bg-kumo-hairline" />} />
<Flow.Node
render={<li className="rounded-full size-4 bg-kumo-hairline" />}
/>
<Flow.Node
render={
<li className="bg-kumo-contrast text-kumo-inverse rounded-lg font-medium py-2 px-3">
Expand Down Expand Up @@ -133,7 +138,9 @@ export function FlowAnchorDemo() {
export function FlowCenteredDemo() {
return (
<Flow align="center">
<Flow.Node render={<li className="rounded-full size-4 bg-kumo-hairline" />} />
<Flow.Node
render={<li className="rounded-full size-4 bg-kumo-hairline" />}
/>
<Flow.Node>my-worker</Flow.Node>
<Flow.Node
render={
Expand Down Expand Up @@ -303,6 +310,28 @@ export function FlowSequentialParallelDemo() {
);
}

/** Flow diagram where a node can be dynamically added and removed */
export function FlowDynamicNodeDemo() {
const [showMiddle, setShowMiddle] = useState(false);

return (
<div className="flex flex-col items-center gap-6">
<button
type="button"
onClick={() => setShowMiddle((v) => !v)}
className="rounded-md px-3 py-1.5 text-sm font-medium ring ring-kumo-line bg-kumo-elevated hover:bg-kumo-base text-kumo-default transition-colors"
>
{showMiddle ? "Remove middle node" : "Add middle node"}
</button>
<Flow>
<Flow.Node>Start</Flow.Node>
{showMiddle && <Flow.Node>Middle</Flow.Node>}
<Flow.Node>End</Flow.Node>
</Flow>
</div>
);
}

/** Flow diagram with expandable nodes in a parallel group */
export function FlowExpandableDemo() {
return (
Expand Down
13 changes: 13 additions & 0 deletions packages/kumo-docs-astro/src/pages/tests/flow.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
FlowParallelNestedListDemo,
FlowSequentialParallelDemo,
FlowExpandableDemo,
FlowDynamicNodeDemo,
} from "../../components/demos/FlowDemo";
---

Expand Down Expand Up @@ -126,6 +127,18 @@ import {
</div>
</section>

<section class="space-y-4">
<h2 class="text-xl font-semibold text-kumo-default">
FlowDynamicNodeDemo
</h2>
<p class="text-kumo-subtle">
Flow diagram with a node that can be added and removed dynamically
</p>
<div class="p-4 rounded-lg border border-kumo-hairline bg-kumo-base">
<FlowDynamicNodeDemo client:load />
</div>
</section>

<section class="space-y-4">
<h2 class="text-xl font-semibold text-kumo-default">
FlowExpandableDemo
Expand Down
52 changes: 52 additions & 0 deletions packages/kumo/src/components/flow/connectors.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { forwardRef, useId, type ReactNode } from "react";
import type { Edges, NodePositions, FlowState } from "./flow-layout";

export interface Connector {
x1: number;
Expand Down Expand Up @@ -151,6 +152,57 @@ export function createRoundedPath(
return commands.join(" ");
}

// =============================================================================
// FlowConnectors
// =============================================================================

type FlowConnectorsProps = {
edges: Edges;
nodePositions: NodePositions;
nodes: FlowState["nodes"];
};

/**
* Draws every edge in the flow using only computed positions and measured
* node sizes — no DOM rect lookups needed.
*
* Each edge connects the right-center of the source node to the left-center
* of the target node.
*
* Intended to be rendered once at the top-level Flow component, absolutely
* positioned to overlay the entire diagram.
*/
export function FlowConnectors({
edges,
nodePositions,
nodes,
}: FlowConnectorsProps) {
const connectors: Connector[] = [];

for (const [fromId, toId] of edges) {
const fromPos = nodePositions[fromId];
const toPos = nodePositions[toId];
const fromNode = nodes[fromId];
const toNode = nodes[toId];

if (!fromPos || !toPos || !fromNode || !toNode) continue;

connectors.push({
// right edge of the source node; Y uses anchor midpoint when available
x1: fromPos.x + fromNode.width,
y1: fromPos.y + (fromNode.startAnchorOffset ?? fromNode.height / 2),
// left edge of the target node; Y uses anchor midpoint when available
x2: toPos.x,
y2: toPos.y + (toNode.endAnchorOffset ?? toNode.height / 2),
fromId,
toId,
single: true,
});
}

return <Connectors connectors={connectors} orientation="horizontal" />;
}

export const Connectors = forwardRef<SVGSVGElement, ConnectorsProps>(
function Connectors({ connectors, children, ...pathProps }, svgRef) {
const id = useId();
Expand Down
Loading