Refactor UI to light theme, move controls to header, and add collapsible sidebar

This commit is contained in:
2025-12-21 21:44:16 +09:00
parent 78343d2b9a
commit c3ae845ac9
4 changed files with 353 additions and 363 deletions

View File

@@ -15,7 +15,7 @@ const Terminal: React.FC = () => {
const [input, setInput] = useState('');
const [autoScroll, setAutoScroll] = useState(true);
const logsContainerRef = useRef<HTMLDivElement>(null);
const MAX_LOGS = 200;
useEffect(() => {
@@ -30,9 +30,9 @@ const Terminal: React.FC = () => {
};
// NEWEST FIRST: Prepend new log to the beginning of the array
const newLogs = [newLog, ...prev];
if (newLogs.length > MAX_LOGS) {
return newLogs.slice(0, MAX_LOGS);
return newLogs.slice(0, MAX_LOGS);
}
return newLogs;
});
@@ -57,85 +57,85 @@ const Terminal: React.FC = () => {
const hexStr = input.replace(/\s+/g, '');
if (!/^[0-9A-Fa-f]+$/.test(hexStr) || hexStr.length % 2 !== 0) {
// Local error log (prepended)
setLogs(prev => [{
id: Date.now(),
type: 'error',
timestamp: new Date().toLocaleTimeString(),
data: '유효하지 않은 HEX 포맷입니다.'
}, ...prev]);
setLogs(prev => [{
id: Date.now(),
type: 'error',
timestamp: new Date().toLocaleTimeString(),
data: '유효하지 않은 HEX 포맷입니다.'
}, ...prev]);
return;
}
const bytes = new Uint8Array(hexStr.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
try {
await serialService.sendRaw(bytes);
// readRaw also triggers callback inside service
await serialService.readRaw();
await serialService.sendRaw(bytes);
// readRaw also triggers callback inside service
await serialService.readRaw();
} catch (e: any) {
// Errors from service are logged via callback usually
// Errors from service are logged via callback usually
}
setInput('');
};
return (
<div className="flex flex-col h-full bg-gray-950/50">
<div className="flex justify-between items-center p-3 border-b border-gray-800 bg-gray-900/50">
<div className="flex flex-col h-full bg-gray-50">
<div className="flex justify-between items-center p-3 border-b border-gray-200 bg-white">
<div className="flex items-center gap-2 overflow-hidden">
<TerminalIcon size={18} className="text-gray-400 shrink-0" />
<span className="font-semibold text-gray-300 truncate"> / Logs</span>
<TerminalIcon size={18} className="text-gray-500 shrink-0" />
<span className="font-semibold text-gray-700 truncate">Terminal / Logs</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setAutoScroll(!autoScroll)}
className={`p-1.5 rounded transition-colors ${autoScroll ? 'text-green-400 bg-green-900/20' : 'text-gray-500 hover:text-gray-300'}`}
title={autoScroll ? "Auto-scroll: ON (Top)" : "Auto-scroll: OFF"}
>
{/* Icon indicates scroll direction focus */}
<ArrowUp size={16} className={autoScroll ? "" : "opacity-50"} />
</button>
<button onClick={() => setLogs([])} className="p-1.5 text-gray-400 hover:text-red-400 hover:bg-red-900/20 rounded transition-colors" title="Clear Logs">
<button
onClick={() => setAutoScroll(!autoScroll)}
className={`p-1.5 rounded transition-colors ${autoScroll ? 'text-green-600 bg-green-100' : 'text-gray-400 hover:text-gray-600'}`}
title={autoScroll ? "Auto-scroll: ON (Top)" : "Auto-scroll: OFF"}
>
{/* Icon indicates scroll direction focus */}
<ArrowUp size={16} className={autoScroll ? "" : "opacity-50"} />
</button>
<button onClick={() => setLogs([])} className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-100 rounded transition-colors" title="Clear Logs">
<Trash2 size={16} />
</button>
</button>
</div>
</div>
<div className="flex-1 bg-black/50 overflow-hidden flex flex-col font-mono text-xs shadow-inner">
<div className="flex-1 bg-white overflow-hidden flex flex-col font-mono text-xs shadow-inner">
{/* Input Area (Top) or Bottom? Keeping input at bottom is standard, logs flow down from top */}
<div
ref={logsContainerRef}
className="flex-1 overflow-y-auto p-2 space-y-1 scroll-smooth"
<div
ref={logsContainerRef}
className="flex-1 overflow-y-auto p-2 space-y-1 scroll-smooth"
>
{logs.length === 0 && <div className="text-gray-700 text-center mt-4 italic"> ... (Logs)</div>}
{logs.length === 0 && <div className="text-gray-500 text-center mt-4 italic"> ... (Logs)</div>}
{logs.map(log => (
<div key={log.id} className="flex gap-2 animate-in fade-in slide-in-from-top-1 hover:bg-gray-900/30 p-0.5 rounded border-b border-gray-900/50 last:border-0">
<span className="text-gray-600 shrink-0 select-none">[{log.timestamp}]</span>
<div key={log.id} className="flex gap-2 animate-in fade-in slide-in-from-top-1 hover:bg-gray-100 p-0.5 rounded border-b border-gray-100 last:border-0">
<span className="text-gray-500 shrink-0 select-none">[{log.timestamp}]</span>
<div className="break-all flex-1">
{log.type === 'tx' && <span className="text-blue-400 font-bold mr-1">TX</span>}
{log.type === 'rx' && <span className="text-green-400 font-bold mr-1">RX</span>}
{log.type === 'tx' && <span className="text-blue-600 font-bold mr-1">TX</span>}
{log.type === 'rx' && <span className="text-green-600 font-bold mr-1">RX</span>}
{log.type === 'error' && <span className="text-red-500 font-bold mr-1">ERR</span>}
{log.type === 'info' && <span className="text-gray-400 font-bold mr-1">INF</span>}
<span className={`${log.type === 'error' ? 'text-red-400' : 'text-gray-300'}`}>
{log.data}
{log.type === 'info' && <span className="text-gray-500 font-bold mr-1">INF</span>}
<span className={`${log.type === 'error' ? 'text-red-600' : 'text-gray-800'}`}>
{log.data}
</span>
</div>
</div>
))}
</div>
<div className="p-2 border-t border-gray-800 bg-gray-900/30">
<div className="p-2 border-t border-gray-200 bg-gray-50">
<div className="flex gap-2">
<input
type="text"
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
placeholder="HEX (e.g. DD A5 03...)"
className="flex-1 bg-gray-950 border border-gray-700 rounded px-2 py-1.5 text-gray-200 focus:outline-none focus:border-blue-500 font-mono text-xs"
className="flex-1 bg-white border border-gray-300 rounded px-2 py-1.5 text-gray-900 focus:outline-none focus:border-blue-500 font-mono text-xs"
/>
<button
<button
onClick={handleSend}
className="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1.5 rounded flex items-center gap-1 transition-colors text-xs font-bold"
>