From c3ae845ac9a5cf4ad8b7335cbb66ef8fdfa3febe Mon Sep 17 00:00:00 2001 From: arDTDev Date: Sun, 21 Dec 2025 21:44:16 +0900 Subject: [PATCH] Refactor UI to light theme, move controls to header, and add collapsible sidebar --- App.tsx | 105 ++++----- components/Dashboard.tsx | 489 +++++++++++++++++++-------------------- components/Settings.tsx | 26 +-- components/Terminal.tsx | 96 ++++---- 4 files changed, 353 insertions(+), 363 deletions(-) diff --git a/App.tsx b/App.tsx index 43f0866..e7ddf1e 100644 --- a/App.tsx +++ b/App.tsx @@ -6,7 +6,7 @@ import { BMSBasicInfo, BMSCellInfo, ConnectionState } from './types'; import Dashboard from './components/Dashboard'; import Settings from './components/Settings'; import Terminal from './components/Terminal'; -import { LayoutDashboard, Settings as SettingsIcon, Usb, AlertCircle, RefreshCw } from 'lucide-react'; +import { LayoutDashboard, Settings as SettingsIcon, Usb, AlertCircle, RefreshCw, PanelRightClose, PanelRightOpen } from 'lucide-react'; const App: React.FC = () => { const [connectionState, setConnectionState] = useState(ConnectionState.DISCONNECTED); @@ -15,6 +15,7 @@ const App: React.FC = () => { const [hwVersion, setHwVersion] = useState(null); const [activeTab, setActiveTab] = useState<'dashboard' | 'settings'>('dashboard'); const [errorMsg, setErrorMsg] = useState(null); + const [isTerminalOpen, setIsTerminalOpen] = useState(true); // Polling Logic useEffect(() => { @@ -131,15 +132,15 @@ const App: React.FC = () => { }; return ( -
+
{/* Sidebar */} -
+
J
- JBD Tool + JBD Tool
@@ -179,57 +180,53 @@ const App: React.FC = () => {
-
-
- {connectionState === ConnectionState.CONNECTED ? ( - - ) : ( - - )} -
+
+ {/* AdSense Area - Expanded */}
{/* Main Content Area */}
{/* Header */} -
-

- {activeTab === 'dashboard' ? '개요 (Overview)' : '설정 (Configuration)'} +
+

+ {activeTab === 'dashboard' ? 'Overview' : 'Configuration'}

- {connectionState === ConnectionState.CONNECTED && ( -
-
- CONNECTED -
- )} - {connectionState === ConnectionState.DISCONNECTED && ( -
-
- OFFLINE -
+ {connectionState === ConnectionState.CONNECTED ? ( + + ) : ( + )} + + +
{/* Content Body with Right Sidebar */}
{/* Main View */} -
+
{errorMsg && (
@@ -241,27 +238,23 @@ const App: React.FC = () => { {/* Empty State Overlay */} {connectionState !== ConnectionState.CONNECTED && ( -
-
+
+
-

장치가 연결되지 않았습니다

-

+

장치가 연결되지 않았습니다

+

JBD BMS를 UART-to-USB 어댑터로 연결하여 실시간 상태를 확인하고 설정을 변경하세요.

-
)}
{/* Right Terminal Panel */} -

diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx index 4ffaef4..3451fbd 100644 --- a/components/Dashboard.tsx +++ b/components/Dashboard.tsx @@ -12,28 +12,28 @@ interface DashboardProps { } const COLORS = [ - '#ef4444', '#f97316', '#f59e0b', '#84cc16', '#10b981', - '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#d946ef', + '#ef4444', '#f97316', '#f59e0b', '#84cc16', '#10b981', + '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#d946ef', '#f43f5e', '#fbbf24', '#a3e635', '#34d399', '#22d3ee', '#94a3b8' ]; // Updated MetricCard to accept valueClassName for custom styling (e.g., blinking) const MetricCard: React.FC<{ label: string; value: string; unit?: string; icon: React.ReactNode; color?: string; subValue?: string; valueClassName?: string }> = ({ label, value, unit, icon, color = "text-blue-400", subValue, valueClassName }) => ( -
+
- {React.isValidElement(icon) && React.cloneElement(icon as React.ReactElement, { size: 48 })} + {React.isValidElement(icon) && React.cloneElement(icon as React.ReactElement, { size: 48 })}
- {label} + {label}
{icon}
- {value} - {unit && {unit}} + {value} + {unit && {unit}}
- {subValue &&
{subValue}
} + {subValue &&
{subValue}
}
); @@ -42,13 +42,11 @@ const MosfetSwitch: React.FC<{ label: string; active: boolean; onClick: () => vo + {/* Capacity Info */} +
+

+ + Capacity Info +

+
+
+ Design Capacity + {basicInfo ? basicInfo.fullCapacity.toFixed(2) : '-.--'} Ah +
+
+ Remaining Cap + {basicInfo ? basicInfo.remainingCapacity.toFixed(2) : '-.--'} Ah +
+
+ Mfg Date + {basicInfo ? basicInfo.productionDate : 'YYYY-MM-DD'} +
+
+ Device Ver + {hwVersion || '--'} +
+
+ + {/* Logger */} +
+
+

Data Logger

+ {isRecording && ● REC ({logCount})} +
+ +
diff --git a/components/Settings.tsx b/components/Settings.tsx index a9b2053..39d9503 100644 --- a/components/Settings.tsx +++ b/components/Settings.tsx @@ -163,9 +163,9 @@ const Settings: React.FC = () => { // 그룹 컴포넌트 const Group: React.FC<{ title: string, children: React.ReactNode }> = ({ title, children }) => ( -
-
-

+
+
+

{title}

@@ -226,10 +226,10 @@ const Settings: React.FC = () => { }; return ( -
+
-
{label}
-
ADDR: 0x{reg.toString(16).toUpperCase().padStart(2, '0')}
+
{label}
+
ADDR: 0x{reg.toString(16).toUpperCase().padStart(2, '0')}
@@ -240,16 +240,16 @@ const Settings: React.FC = () => { onChange={handleChange} placeholder={val === undefined ? "?" : ""} disabled={isBusy || globalLoading} - className={`bg-gray-900 border ${isDirty ? 'border-yellow-600' : 'border-gray-700'} text-gray-100 text-right px-3 py-1.5 rounded focus:outline-none focus:border-blue-500 font-mono h-9 ${type === 'string' ? 'w-40' : 'w-28'}`} + className={`bg-white border ${isDirty ? 'border-yellow-600' : 'border-gray-300'} text-gray-900 text-right px-3 py-1.5 rounded focus:outline-none focus:border-blue-500 font-mono h-9 ${type === 'string' ? 'w-40' : 'w-28'}`} /> - {unit && {type !== 'date' ? unit : ''}} + {unit && {type !== 'date' ? unit : ''}}
@@ -258,7 +258,7 @@ const Settings: React.FC = () => { onClick={onSave} disabled={isBusy || globalLoading} title="개별 저장" - className={`p-2 rounded-lg transition-all disabled:opacity-50 ${isDirty ? 'bg-blue-600 text-white hover:bg-blue-500' : 'bg-gray-800 text-gray-500 hover:text-gray-300'}`} + className={`p-2 rounded-lg transition-all disabled:opacity-50 ${isDirty ? 'bg-blue-600 text-white hover:bg-blue-500' : 'bg-gray-100 text-gray-500 hover:text-gray-700'}`} > @@ -270,10 +270,10 @@ const Settings: React.FC = () => { return (
{/* 상단 툴바 */} -
+
-

EEPROM 설정

-

BMS 내부 파라미터를 개별적으로 읽거나 수정할 수 있습니다.

+

EEPROM 설정

+

BMS 내부 파라미터를 개별적으로 읽거나 수정할 수 있습니다.

- + +
-
+
{/* Input Area (Top) or Bottom? Keeping input at bottom is standard, logs flow down from top */} - -
- {logs.length === 0 &&
대기 중... (Logs)
} - + {logs.length === 0 &&
대기 중... (Logs)
} + {logs.map(log => ( -
- [{log.timestamp}] +
+ [{log.timestamp}]
- {log.type === 'tx' && TX} - {log.type === 'rx' && RX} + {log.type === 'tx' && TX} + {log.type === 'rx' && RX} {log.type === 'error' && ERR} - {log.type === 'info' && INF} - - {log.data} + {log.type === 'info' && INF} + + {log.data}
))}
- -
+ +
- 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" /> -