Initial commit: Industrial HMI system with component architecture
- 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>
This commit is contained in:
154
frontend/components/SettingsModal.tsx
Normal file
154
frontend/components/SettingsModal.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React from 'react';
|
||||
import { Settings, RotateCw, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { ConfigItem } from '../types';
|
||||
|
||||
interface SettingsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
config: ConfigItem[] | null;
|
||||
isRefreshing: boolean;
|
||||
onSave: (config: ConfigItem[]) => void;
|
||||
}
|
||||
|
||||
const TechButton = ({ children, onClick, active = false, variant = 'blue', className = '' }: any) => {
|
||||
const colors: any = {
|
||||
blue: 'from-blue-600 to-cyan-600 hover:shadow-glow-blue border-cyan-400/30',
|
||||
red: 'from-red-600 to-pink-600 hover:shadow-glow-red border-red-400/30',
|
||||
amber: 'from-amber-500 to-orange-600 hover:shadow-orange-500/50 border-orange-400/30',
|
||||
green: 'from-emerald-500 to-green-600 hover:shadow-green-500/50 border-green-400/30'
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`
|
||||
relative px-4 py-2 font-tech font-bold tracking-wider uppercase transition-all duration-300
|
||||
clip-tech border-b-2 border-r-2
|
||||
${active ? `bg-gradient-to-r ${colors[variant]} text-white` : 'bg-slate-800/50 text-slate-400 hover:text-white hover:bg-slate-700/50 border-slate-600'}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{active && <div className="absolute inset-0 bg-white/20 animate-pulse pointer-events-none"></div>}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, config, isRefreshing, onSave }) => {
|
||||
const [localConfig, setLocalConfig] = React.useState<ConfigItem[]>([]);
|
||||
const [expandedGroups, setExpandedGroups] = React.useState<Set<string>>(new Set());
|
||||
|
||||
React.useEffect(() => {
|
||||
if (config) {
|
||||
setLocalConfig(config);
|
||||
// Auto-expand all groups initially
|
||||
const groups = new Set(config.map(c => c.Group));
|
||||
setExpandedGroups(groups);
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
const handleChange = (idx: number, newValue: string) => {
|
||||
setLocalConfig(prev => {
|
||||
const next = [...prev];
|
||||
next[idx] = { ...next[idx], Value: newValue };
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleGroup = (group: string) => {
|
||||
setExpandedGroups(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(group)) next.delete(group);
|
||||
else next.add(group);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Group items by category
|
||||
const groupedConfig = React.useMemo(() => {
|
||||
const groups: { [key: string]: { item: ConfigItem, originalIdx: number }[] } = {};
|
||||
localConfig.forEach((item, idx) => {
|
||||
if (!groups[item.Group]) groups[item.Group] = [];
|
||||
groups[item.Group].push({ item, originalIdx: idx });
|
||||
});
|
||||
return groups;
|
||||
}, [localConfig]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="w-[900px] glass-holo p-8 border border-neon-blue shadow-glow-blue relative flex flex-col max-h-[90vh]">
|
||||
<button onClick={onClose} className="absolute top-4 right-4 text-slate-400 hover:text-white">✕</button>
|
||||
<h2 className="text-2xl font-tech font-bold text-neon-blue mb-8 border-b border-white/10 pb-4 flex items-center gap-3 flex-none">
|
||||
<Settings className="animate-spin-slow" /> SYSTEM CONFIGURATION
|
||||
</h2>
|
||||
|
||||
{isRefreshing ? (
|
||||
<div className="h-64 flex flex-col items-center justify-center gap-4 animate-pulse flex-1">
|
||||
<RotateCw className="w-12 h-12 text-neon-blue animate-spin" />
|
||||
<div className="text-xl font-tech text-neon-blue tracking-widest">FETCHING CONFIGURATION...</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar pr-2 mb-8">
|
||||
<div className="space-y-6">
|
||||
{Object.entries(groupedConfig).map(([groupName, items]) => (
|
||||
<div key={groupName} className="border border-white/10 bg-black/20">
|
||||
<button
|
||||
onClick={() => toggleGroup(groupName)}
|
||||
className="w-full flex items-center gap-2 p-3 bg-white/5 hover:bg-white/10 transition-colors text-left"
|
||||
>
|
||||
{expandedGroups.has(groupName) ? <ChevronDown className="w-4 h-4 text-neon-blue" /> : <ChevronRight className="w-4 h-4 text-slate-400" />}
|
||||
<span className="font-tech font-bold text-lg text-white tracking-wider">{groupName}</span>
|
||||
<span className="text-xs text-slate-500 ml-auto">{items.length} ITEMS</span>
|
||||
</button>
|
||||
|
||||
{expandedGroups.has(groupName) && (
|
||||
<div className="p-4 space-y-4">
|
||||
{items.map(({ item, originalIdx }) => (
|
||||
<div key={originalIdx} className="grid grid-cols-[250px_1fr] gap-6 items-start group">
|
||||
<div>
|
||||
<div className="text-sm font-bold text-neon-blue mb-1">{item.Key}</div>
|
||||
<div className="text-xs text-slate-400 leading-tight">{item.Description}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{item.Type === 'Boolean' ? (
|
||||
<div className="flex items-center gap-3 h-full">
|
||||
<button
|
||||
onClick={() => handleChange(originalIdx, item.Value === 'true' ? 'false' : 'true')}
|
||||
className={`w-12 h-6 rounded-full p-1 transition-colors ${item.Value === 'true' ? 'bg-neon-green' : 'bg-slate-700'}`}
|
||||
>
|
||||
<div className={`w-4 h-4 rounded-full bg-white shadow transition-transform ${item.Value === 'true' ? 'translate-x-6' : 'translate-x-0'}`} />
|
||||
</button>
|
||||
<span className={`font-mono text-sm font-bold ${item.Value === 'true' ? 'text-neon-green' : 'text-slate-400'}`}>
|
||||
{item.Value.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type={item.Type === 'Number' ? 'number' : 'text'}
|
||||
value={item.Value}
|
||||
onChange={(e) => handleChange(originalIdx, e.target.value)}
|
||||
className="w-full bg-black/50 border border-slate-700 text-white font-mono text-sm px-3 py-2 focus:border-neon-blue focus:outline-none transition-colors hover:border-slate-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-4 flex-none pt-4 border-t border-white/10">
|
||||
<TechButton onClick={onClose}>CANCEL</TechButton>
|
||||
<TechButton variant="blue" active onClick={() => { onSave(localConfig); onClose(); }}>SAVE CONFIG</TechButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user