Files
codetutor/frontend/src/components/visualizations-new/primitives/decision-node.tsx

110 lines
2.7 KiB
TypeScript

'use client';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import type { DecisionNodeState } from '@/lib/visualizations/types';
interface DecisionNodeProps {
node: DecisionNodeState;
x: number;
y: number;
width?: number;
height?: number;
isInPath?: boolean;
className?: string;
}
const STATE_CLASSES = {
normal: 'fill-[var(--surface-variant)] stroke-[var(--border)]',
exploring: 'fill-[var(--info)]/20 stroke-[var(--info)]',
complete: 'fill-[var(--success)]/20 stroke-[var(--success)]',
backtracking: 'fill-[var(--warning)]/20 stroke-[var(--warning)]',
current: 'fill-[var(--primary)]/20 stroke-[var(--primary)]',
} as const;
const TEXT_CLASSES = {
normal: 'fill-[var(--foreground)]',
exploring: 'fill-[var(--info)]',
complete: 'fill-[var(--success)]',
backtracking: 'fill-[var(--warning)]',
current: 'fill-[var(--primary)]',
} as const;
export function DecisionNode({
node,
x,
y,
width = 64,
height = 32,
isInPath = false,
className,
}: DecisionNodeProps) {
const isActive = node.state === 'current' || node.state === 'exploring';
const showDecision = node.decision && node.state === 'current';
return (
<motion.g
initial={false}
animate={{
scale: isActive ? 1.05 : 1,
}}
transition={{
type: 'spring',
stiffness: 300,
damping: 30,
}}
style={{ transformOrigin: `${x}px ${y}px` }}
className={cn(className)}
>
{/* Decision label above node */}
{showDecision && (
<motion.text
x={x}
y={y - height / 2 - 12}
textAnchor="middle"
className="fill-[var(--primary)] text-xs font-medium"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
{node.decision}
</motion.text>
)}
{/* Node rectangle (rounded) */}
<motion.rect
x={x - width / 2}
y={y - height / 2}
width={width}
height={height}
rx={6}
ry={6}
strokeWidth={isInPath ? 2.5 : 2}
className={cn(
'transition-colors duration-200',
STATE_CLASSES[node.state]
)}
initial={false}
animate={{
filter: isActive ? 'drop-shadow(0 0 8px var(--primary))' : 'none',
}}
transition={{ duration: 0.2 }}
/>
{/* Value text */}
<text
x={x}
y={y}
textAnchor="middle"
dominantBaseline="central"
className={cn(
'pointer-events-none select-none font-mono text-xs font-medium',
TEXT_CLASSES[node.state]
)}
>
{node.value}
</text>
</motion.g>
);
}