Flow Diagram
Diagramme de flux interactif avec noeuds, liens et controles. Installez Flow Diagram depuis le registre VLLNT UI avec la CLI shadcn.
Diagramme de flux interactif avec noeuds, liens et controles. Fait partie de la famille data dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.
Installation
Ajoutez Flow Diagram a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/flow-diagram.jsonSource
"use client";
import "@xyflow/react/dist/style.css";
import { memo, useCallback, useEffect } from "react";
import {
type Node,
type NodeMouseHandler,
ReactFlowProvider,
} from "@xyflow/react";
import { cn } from "../../lib/utils";
import { FlowCanvas } from "./flow-canvas";
import { FlowErrorBoundary } from "./flow-error-boundary";
import { FlowFullscreen } from "./flow-fullscreen";
import type {
FlowDiagramEdge,
FlowDiagramNode,
FlowDiagramProps,
} from "./types";
import { useFlowDiagram } from "./use-flow-diagram";
/**
* Validates nodes and edges, logging warnings for common issues.
*/
function validateFlowData(
nodes: FlowDiagramNode[],
edges: FlowDiagramEdge[],
): void {
if (nodes.length === 0 && edges.length > 0) {
console.warn(
"[FlowDiagram] Edges provided without nodes - edges will not render",
);
}
const nodeIds = new Set(nodes.map((n) => n.id));
const invalidEdges = edges.filter(
(e) => !nodeIds.has(e.source) || !nodeIds.has(e.target),
);
if (invalidEdges.length > 0) {
console.warn(
`[FlowDiagram] ${invalidEdges.length} edge(s) reference non-existent nodes:`,
invalidEdges.map((e) => `${e.id}: ${e.source} -> ${e.target}`),
);
}
const nodesWithoutPosition = nodes.filter((n) => n.position === undefined);
if (nodesWithoutPosition.length > 0) {
console.warn(
`[FlowDiagram] ${nodesWithoutPosition.length} node(s) missing position:`,
nodesWithoutPosition.map((n) => n.id),
);
}
}
const FlowDiagramInner = memo(function FlowDiagramInner({
allowCopy = false,
allowFullscreen = true,
className,
edges,
fitView = true,
fitViewOptions,
height = 400,
nodes,
onNodeClick,
showControls = true,
title,
}: FlowDiagramProps) {
// Check input on mount and when data changes
useEffect(() => {
validateFlowData(nodes, edges);
}, [nodes, edges]);
const {
closeFullscreen,
copyStatus,
copyToClipboard,
fitView: handleFitView,
isFullscreen,
toggleFullscreen,
zoomIn,
zoomOut,
} = useFlowDiagram({ allowCopy, allowFullscreen });
// Memoize node click handler to prevent unnecessary re-renders
const handleNodeClick: NodeMouseHandler | undefined = useCallback(
(_event: React.MouseEvent, node: Node) => {
onNodeClick?.(node as FlowDiagramNode);
},
[onNodeClick],
);
const handleCopy = useCallback(() => {
void copyToClipboard();
}, [copyToClipboard]);
const canvasProps = {
allowCopy,
allowFullscreen,
className,
copyStatus,
edges,
fitView,
fitViewOptions,
height,
isFullscreen,
nodes,
onCopy: allowCopy ? handleCopy : undefined,
onFitView: handleFitView,
onFullscreen: allowFullscreen ? toggleFullscreen : undefined,
onNodeClick: onNodeClick ? handleNodeClick : undefined,
onZoomIn: zoomIn,
onZoomOut: zoomOut,
showControls,
title,
};
if (isFullscreen) {
return (
<>
<div
className={cn(
"rounded-lg border border-border bg-muted/50",
className,
)}
style={{ height }}
/>
<FlowFullscreen isOpen={isFullscreen} onClose={closeFullscreen}>
<FlowCanvas {...canvasProps} />
</FlowFullscreen>
</>
);
}
return <FlowCanvas {...canvasProps} />;
});
/**
* FlowDiagram component for rendering interactive flow diagrams.
* Uses @xyflow/react under the hood with error boundary protection.
*
* @example
* ```tsx
* <FlowDiagram
* nodes={[
* { id: '1', data: { label: 'Start' }, position: { x: 0, y: 0 } },
* { id: '2', data: { label: 'End' }, position: { x: 200, y: 100 } }
* ]}
* edges={[{ id: 'e1-2', source: '1', target: '2' }]}
* showControls
* allowFullscreen
* />
* ```
*/
export const FlowDiagram = memo(function FlowDiagram(props: FlowDiagramProps) {
return (
<FlowErrorBoundary height={props.height}>
<ReactFlowProvider>
<FlowDiagramInner {...props} />
</ReactFlowProvider>
</FlowErrorBoundary>
);
});
Stories
Explorez chaque variante et etat dans le Storybook interactif :
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.