Advanced pages. Model-driven controls.

This commit is contained in:
2026-07-11 02:31:14 +00:00
parent cd2f28df34
commit 1f3fcf50b4
23 changed files with 1600 additions and 444 deletions

View File

@@ -0,0 +1,91 @@
import { forwardRef } from 'react';
import { cn } from '@/lib/utils';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
export type NodeStatus = 'ok' | 'error' | 'na' | 'inactive';
interface TopologyNodeProps {
label: string;
sublabel?: string;
status?: NodeStatus;
icon?: React.ReactNode;
onClick?: () => void;
interactive?: boolean;
disabled?: boolean;
tooltip?: React.ReactNode;
className?: string;
children?: React.ReactNode;
}
const statusColors: Record<NodeStatus, string> = {
ok: 'border-green-500/60 bg-green-50/50 dark:bg-green-950/20',
error: 'border-red-500/60 bg-red-50/50 dark:bg-red-950/20',
na: 'border-muted-foreground/30 bg-muted/30',
inactive: 'border-dashed border-muted-foreground/20 bg-muted/10 opacity-50',
};
const statusDotColors: Record<NodeStatus, string> = {
ok: 'bg-green-500',
error: 'bg-red-500',
na: 'bg-muted-foreground/40',
inactive: 'bg-muted-foreground/20',
};
export const TopologyNode = forwardRef<HTMLDivElement, TopologyNodeProps>(
function TopologyNode(
{ label, sublabel, status = 'ok', icon, onClick, interactive, disabled, tooltip, className, children },
ref,
) {
const isClickable = (interactive || onClick) && !disabled;
const content = (
<div
ref={ref}
role={isClickable ? 'button' : undefined}
tabIndex={isClickable ? 0 : undefined}
onClick={isClickable ? onClick : undefined}
// Stop pointerdown propagation so React Flow doesn't swallow the click
onPointerDown={isClickable ? (e) => e.stopPropagation() : undefined}
onKeyDown={isClickable ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick?.(); } } : undefined}
style={{ pointerEvents: 'all', position: 'relative' }}
className={cn(
'rounded-lg border px-3 py-2 text-xs transition-all duration-200',
'nopan nodrag',
statusColors[status],
isClickable && '!cursor-pointer hover:shadow-sm hover:border-primary/40',
disabled && '!pointer-events-none',
className,
)}
>
<div className="flex items-center gap-2">
{icon && <span className="shrink-0 text-muted-foreground">{icon}</span>}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<span className={cn('h-1.5 w-1.5 rounded-full shrink-0', statusDotColors[status])} />
<span className="font-medium truncate">{label}</span>
</div>
{sublabel && (
<div className="text-[10px] text-muted-foreground mt-0.5 truncate">{sublabel}</div>
)}
</div>
</div>
{children}
</div>
);
if (tooltip) {
return (
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-64 text-xs">
{tooltip}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return content;
},
);