From 855cff07c20a4c81e7cbf2bbb6ed82943b71759b Mon Sep 17 00:00:00 2001 From: chihlasm Date: Sat, 4 Apr 2026 14:20:42 +0000 Subject: [PATCH] feat: add React Flow UI foundation components for network diagrams BaseNode (structured node shell with header/content/footer slots), BaseHandle (styled connection handle), LabeledHandle (handle with port label), NodeStatusIndicator (status border effect), NodeTooltip (hover details via NodeToolbar). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/network/ui/base-handle.tsx | 20 +++++ .../src/components/network/ui/base-node.tsx | 56 ++++++++++++++ .../components/network/ui/labeled-handle.tsx | 39 ++++++++++ .../network/ui/node-status-indicator.tsx | 43 +++++++++++ .../components/network/ui/node-tooltip.tsx | 77 +++++++++++++++++++ 5 files changed, 235 insertions(+) create mode 100644 frontend/src/components/network/ui/base-handle.tsx create mode 100644 frontend/src/components/network/ui/base-node.tsx create mode 100644 frontend/src/components/network/ui/labeled-handle.tsx create mode 100644 frontend/src/components/network/ui/node-status-indicator.tsx create mode 100644 frontend/src/components/network/ui/node-tooltip.tsx diff --git a/frontend/src/components/network/ui/base-handle.tsx b/frontend/src/components/network/ui/base-handle.tsx new file mode 100644 index 00000000..d70d468f --- /dev/null +++ b/frontend/src/components/network/ui/base-handle.tsx @@ -0,0 +1,20 @@ +import type { ComponentProps } from 'react' +import { Handle, type HandleProps } from '@xyflow/react' +import { cn } from '@/lib/utils' + +export type BaseHandleProps = HandleProps + +export function BaseHandle({ className, children, ...props }: ComponentProps) { + return ( + + {children} + + ) +} diff --git a/frontend/src/components/network/ui/base-node.tsx b/frontend/src/components/network/ui/base-node.tsx new file mode 100644 index 00000000..7a45868f --- /dev/null +++ b/frontend/src/components/network/ui/base-node.tsx @@ -0,0 +1,56 @@ +import type { ComponentProps } from 'react' +import { cn } from '@/lib/utils' + +export function BaseNode({ className, ...props }: ComponentProps<'div'>) { + return ( +
+ ) +} + +export function BaseNodeHeader({ className, ...props }: ComponentProps<'header'>) { + return ( +
+ ) +} + +export function BaseNodeHeaderTitle({ className, ...props }: ComponentProps<'h3'>) { + return ( +

+ ) +} + +export function BaseNodeContent({ className, ...props }: ComponentProps<'div'>) { + return ( +
+ ) +} + +export function BaseNodeFooter({ className, ...props }: ComponentProps<'div'>) { + return ( +
+ ) +} diff --git a/frontend/src/components/network/ui/labeled-handle.tsx b/frontend/src/components/network/ui/labeled-handle.tsx new file mode 100644 index 00000000..d5cecfd0 --- /dev/null +++ b/frontend/src/components/network/ui/labeled-handle.tsx @@ -0,0 +1,39 @@ +import type { ComponentProps } from 'react' +import { type HandleProps, Position } from '@xyflow/react' +import { cn } from '@/lib/utils' +import { BaseHandle } from './base-handle' + +const flexDirections: Record = { + [Position.Top]: 'flex-col', + [Position.Right]: 'flex-row-reverse justify-end', + [Position.Bottom]: 'flex-col-reverse justify-end', + [Position.Left]: 'flex-row', +} + +export function LabeledHandle({ + className, + labelClassName, + handleClassName, + title, + position, + ...props +}: HandleProps & + ComponentProps<'div'> & { + title: string + handleClassName?: string + labelClassName?: string + }) { + const { ref, ...handleProps } = props + return ( +
+ + +
+ ) +} diff --git a/frontend/src/components/network/ui/node-status-indicator.tsx b/frontend/src/components/network/ui/node-status-indicator.tsx new file mode 100644 index 00000000..96032bce --- /dev/null +++ b/frontend/src/components/network/ui/node-status-indicator.tsx @@ -0,0 +1,43 @@ +import type { ReactNode } from 'react' +import { cn } from '@/lib/utils' + +export type NodeStatus = 'online' | 'offline' | 'degraded' | 'unknown' + +const STATUS_BORDER_COLORS: Record = { + online: 'border-emerald-400', + offline: 'border-red-400', + degraded: 'border-yellow-400', + unknown: '', +} + +const STATUS_GLOW: Record = { + online: 'shadow-[0_0_8px_rgba(52,211,153,0.3)]', + offline: 'shadow-[0_0_8px_rgba(248,113,113,0.3)]', + degraded: 'shadow-[0_0_8px_rgba(250,204,21,0.3)]', + unknown: '', +} + +interface NodeStatusIndicatorProps { + status?: NodeStatus + children: ReactNode + className?: string +} + +export function NodeStatusIndicator({ status = 'unknown', children, className }: NodeStatusIndicatorProps) { + if (status === 'unknown') { + return <>{children} + } + + return ( +
+ {children} +
+ ) +} diff --git a/frontend/src/components/network/ui/node-tooltip.tsx b/frontend/src/components/network/ui/node-tooltip.tsx new file mode 100644 index 00000000..236ae1ac --- /dev/null +++ b/frontend/src/components/network/ui/node-tooltip.tsx @@ -0,0 +1,77 @@ +import { createContext, useContext, useState, useCallback, type ReactNode, type ComponentProps } from 'react' +import { NodeToolbar, type NodeToolbarProps } from '@xyflow/react' +import { cn } from '@/lib/utils' + +interface NodeTooltipContextValue { + visible: boolean + show: () => void + hide: () => void +} + +const NodeTooltipContext = createContext({ + visible: false, + show: () => {}, + hide: () => {}, +}) + +export function NodeTooltip({ children, ...props }: ComponentProps<'div'>) { + const [visible, setVisible] = useState(false) + const show = useCallback(() => setVisible(true), []) + const hide = useCallback(() => setVisible(false), []) + + return ( + +
{children}
+
+ ) +} + +export function NodeTooltipTrigger({ + children, + onMouseEnter, + onMouseLeave, + ...props +}: ComponentProps<'div'>) { + const { show, hide } = useContext(NodeTooltipContext) + + return ( +
{ + show() + onMouseEnter?.(e) + }} + onMouseLeave={(e) => { + hide() + onMouseLeave?.(e) + }} + {...props} + > + {children} +
+ ) +} + +export function NodeTooltipContent({ + className, + position, + children, + ...props +}: Omit & { children: ReactNode }) { + const { visible } = useContext(NodeTooltipContext) + + if (!visible) return null + + return ( + + {children} + + ) +}