Refactor data fetching architecture from centralized App state to component-local data management for improved maintainability and data freshness guarantees. Changes: - SettingsModal: Fetch config data on modal open - RecipePanel: Fetch recipe list on panel open - IOMonitorPage: Fetch IO list on page mount with real-time updates - Remove unnecessary props drilling through component hierarchy - Simplify App.tsx by removing centralized config/recipes state New feature: - Add InitializeModal for sequential axis initialization (X, Y, Z) - Each axis initializes with 3-second staggered start - Progress bar animation for each axis - Auto-close on completion 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
167 lines
9.1 KiB
TypeScript
167 lines
9.1 KiB
TypeScript
import React from 'react';
|
|
import { Settings, RotateCw, ChevronDown, ChevronRight } from 'lucide-react';
|
|
import { ConfigItem } from '../types';
|
|
import { comms } from '../communication';
|
|
|
|
interface SettingsModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
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, onSave }) => {
|
|
const [localConfig, setLocalConfig] = React.useState<ConfigItem[]>([]);
|
|
const [expandedGroups, setExpandedGroups] = React.useState<Set<string>>(new Set());
|
|
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
|
|
|
// Fetch config data when modal opens
|
|
React.useEffect(() => {
|
|
if (isOpen) {
|
|
const fetchConfig = async () => {
|
|
setIsRefreshing(true);
|
|
try {
|
|
const configStr = await comms.getConfig();
|
|
const config: ConfigItem[] = JSON.parse(configStr);
|
|
setLocalConfig(config);
|
|
// Auto-expand all groups initially
|
|
const groups = new Set<string>(config.map(c => c.Group));
|
|
setExpandedGroups(groups);
|
|
} catch (e) {
|
|
console.error('Failed to fetch config:', e);
|
|
}
|
|
setIsRefreshing(false);
|
|
};
|
|
fetchConfig();
|
|
}
|
|
}, [isOpen]);
|
|
|
|
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>
|
|
);
|
|
};
|