feat(dashboard): add FlowNode and StepNode components for domain view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-04-02 12:27:32 +08:00
Unverified
parent 1f2607e83f
commit c6433c0352
2 changed files with 105 additions and 0 deletions
@@ -0,0 +1,52 @@
import { memo } from "react";
import { Handle, Position } from "@xyflow/react";
import type { Node, NodeProps } from "@xyflow/react";
import { useDashboardStore } from "../store";
export interface FlowNodeData extends Record<string, unknown> {
label: string;
summary: string;
entryPoint?: string;
entryType?: string;
stepCount: number;
flowId: string;
}
export type FlowFlowNode = Node<FlowNodeData, "flow-node">;
function FlowNode({ data }: NodeProps<FlowFlowNode>) {
const selectNode = useDashboardStore((s) => s.selectNode);
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
const isSelected = selectedNodeId === data.flowId;
return (
<div
className={`rounded-lg border px-4 py-3 min-w-[240px] max-w-[320px] cursor-pointer transition-all ${
isSelected
? "border-accent bg-accent/10"
: "border-border-medium bg-surface hover:border-accent/50"
}`}
onClick={() => selectNode(data.flowId)}
>
<Handle type="target" position={Position.Left} className="!bg-accent/60 !w-2 !h-2" />
<Handle type="source" position={Position.Right} className="!bg-accent/60 !w-2 !h-2" />
{data.entryPoint && (
<div className="text-[9px] font-mono text-accent/70 mb-1 truncate">
{data.entryPoint}
</div>
)}
<div className="text-xs font-semibold text-text-primary mb-1 truncate">
{data.label}
</div>
<div className="text-[10px] text-text-secondary line-clamp-2">
{data.summary}
</div>
<div className="text-[9px] text-text-muted mt-1">
{data.stepCount} step{data.stepCount !== 1 ? "s" : ""}
</div>
</div>
);
}
export default memo(FlowNode);
@@ -0,0 +1,53 @@
import { memo } from "react";
import { Handle, Position } from "@xyflow/react";
import type { Node, NodeProps } from "@xyflow/react";
import { useDashboardStore } from "../store";
export interface StepNodeData extends Record<string, unknown> {
label: string;
summary: string;
filePath?: string;
stepId: string;
order: number;
}
export type StepFlowNode = Node<StepNodeData, "step-node">;
function StepNode({ data }: NodeProps<StepFlowNode>) {
const selectNode = useDashboardStore((s) => s.selectNode);
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
const isSelected = selectedNodeId === data.stepId;
return (
<div
className={`rounded-lg border px-3 py-2.5 min-w-[180px] max-w-[240px] cursor-pointer transition-all ${
isSelected
? "border-accent bg-accent/10"
: "border-border-subtle bg-elevated hover:border-accent/40"
}`}
onClick={() => selectNode(data.stepId)}
>
<Handle type="target" position={Position.Left} className="!bg-text-muted/40 !w-1.5 !h-1.5" />
<Handle type="source" position={Position.Right} className="!bg-text-muted/40 !w-1.5 !h-1.5" />
<div className="flex items-center gap-1.5 mb-1">
<span className="text-[9px] font-mono text-accent/60 shrink-0">
{data.order}
</span>
<span className="text-[11px] font-medium text-text-primary truncate">
{data.label}
</span>
</div>
<div className="text-[10px] text-text-secondary line-clamp-2">
{data.summary}
</div>
{data.filePath && (
<div className="text-[9px] font-mono text-text-muted mt-1 truncate">
{data.filePath}
</div>
)}
</div>
);
}
export default memo(StepNode);