- Implement WebView2-based HMI frontend with React + TypeScript + Vite - Add C# .NET backend with WebSocket communication layer - Separate UI components into modular structure: * RecipePanel: Recipe selection and management * IOPanel: I/O monitoring and control (32 inputs/outputs) * MotionPanel: Servo control for X/Y/Z axes * CameraPanel: Vision system feed with HUD overlay * SettingsModal: System configuration management - Create reusable UI components (CyberPanel, TechButton, PanelHeader) - Implement dual-mode communication (WebView2 native + WebSocket fallback) - Add 3D visualization with Three.js/React Three Fiber - Fix JSON parsing bug in configuration save handler - Include comprehensive .gitignore for .NET and Node.js projects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
51 lines
2.6 KiB
TypeScript
51 lines
2.6 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Activity } from 'lucide-react';
|
|
import { IOPoint } from '../types';
|
|
import { PanelHeader } from './common/PanelHeader';
|
|
import { TechButton } from './common/TechButton';
|
|
|
|
interface IOPanelProps {
|
|
ioPoints: IOPoint[];
|
|
onToggle: (id: number, type: 'input' | 'output') => void;
|
|
}
|
|
|
|
export const IOPanel: React.FC<IOPanelProps> = ({ ioPoints, onToggle }) => {
|
|
const [tab, setTab] = useState<'in' | 'out'>('in');
|
|
const points = ioPoints.filter(p => p.type === (tab === 'in' ? 'input' : 'output'));
|
|
|
|
return (
|
|
<div className="h-full flex flex-col">
|
|
<PanelHeader title="I/O Monitor" icon={Activity} />
|
|
<div className="flex gap-2 mb-4">
|
|
<TechButton active={tab === 'in'} onClick={() => setTab('in')} className="flex-1">Inputs</TechButton>
|
|
<TechButton active={tab === 'out'} onClick={() => setTab('out')} className="flex-1" variant="blue">Outputs</TechButton>
|
|
</div>
|
|
<div className="grid grid-cols-4 gap-2 overflow-y-auto pr-2 custom-scrollbar pb-4">
|
|
{points.map(p => (
|
|
<div
|
|
key={p.id}
|
|
onClick={() => onToggle(p.id, p.type)}
|
|
className={`
|
|
aspect-square flex flex-col items-center justify-center p-1 cursor-pointer transition-all border
|
|
clip-tech
|
|
${p.state
|
|
? (p.type === 'output'
|
|
? 'bg-neon-green/10 border-neon-green text-neon-green shadow-[0_0_10px_rgba(10,255,0,0.3)]'
|
|
: 'bg-neon-yellow/10 border-neon-yellow text-neon-yellow shadow-[0_0_10px_rgba(255,230,0,0.3)]')
|
|
: 'bg-slate-900/50 border-slate-700 text-slate-600 hover:border-slate-500'}
|
|
`}
|
|
>
|
|
<div className={`w-2 h-2 rounded-full mb-2 ${p.state ? (p.type === 'output' ? 'bg-neon-green' : 'bg-neon-yellow') : 'bg-slate-800'}`}></div>
|
|
<span className="font-mono text-[10px] font-bold">
|
|
{p.type === 'input' ? 'I' : 'Q'}{p.id.toString().padStart(2, '0')}
|
|
</span>
|
|
<span className="text-[8px] text-center uppercase leading-tight mt-1 opacity-80 truncate w-full px-1">
|
|
{p.name.replace(/(Sensor|Door|Lamp)/g, '')}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|