diff --git a/docs/adr/ADR-053-ui-design-system.md b/docs/adr/ADR-053-ui-design-system.md index f1aa8039..bea42aaf 100644 --- a/docs/adr/ADR-053-ui-design-system.md +++ b/docs/adr/ADR-053-ui-design-system.md @@ -2,7 +2,7 @@ | Field | Value | |-------|-------| -| Status | Proposed | +| Status | Accepted | | Date | 2026-03-06 | | Deciders | ruv | | Depends on | ADR-052 (Tauri Desktop Frontend) | diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/index.html b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/index.html index 9b79df2d..f88126d7 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/index.html +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/index.html @@ -4,19 +4,9 @@ RuView Desktop - + + +
diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/App.tsx b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/App.tsx index bdc639cb..99cc0968 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/App.tsx +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/App.tsx @@ -25,9 +25,9 @@ const NAV_ITEMS: NavItem[] = [ { id: "nodes", label: "Nodes", shortcut: "N" }, { id: "flash", label: "Flash", shortcut: "F" }, { id: "ota", label: "OTA", shortcut: "O" }, - { id: "wasm", label: "WASM", shortcut: "W" }, + { id: "wasm", label: "Edge Modules", shortcut: "W" }, { id: "sensing", label: "Sensing", shortcut: "S" }, - { id: "mesh", label: "Mesh", shortcut: "M" }, + { id: "mesh", label: "Mesh View", shortcut: "M" }, { id: "settings", label: "Settings", shortcut: "G" }, ]; @@ -35,115 +35,178 @@ const App: React.FC = () => { const [activePage, setActivePage] = useState("dashboard"); return ( -
- {/* Sidebar */} - + + {/* Main content */} +
- v0.3.0 -
- + {activePage === "dashboard" && } + {activePage === "nodes" && } + {activePage === "flash" && } + {activePage === "settings" && } + {!["dashboard", "nodes", "flash", "settings"].includes(activePage) && ( +
+

+ {NAV_ITEMS.find((n) => n.id === activePage)?.label} +

+

+ This page is not yet implemented. +

+
+ )} + + - {/* Main content */} -
- {activePage === "dashboard" && } - {activePage === "nodes" && } - {activePage === "flash" && } - {activePage === "settings" && } - {!["dashboard", "nodes", "flash", "settings"].includes(activePage) && ( -
-

- {NAV_ITEMS.find((n) => n.id === activePage)?.label} -

-

- This page is not yet implemented. -

-
- )} -
+ {/* Status Bar */} +
+ Powered by rUv + | + + + 0 nodes online + + | + Server: stopped + | + Port: 8080 +
); }; diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/components/NodeCard.tsx b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/components/NodeCard.tsx index 3c89b1eb..42aa2a1c 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/components/NodeCard.tsx +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/components/NodeCard.tsx @@ -17,8 +17,7 @@ function formatUptime(secs: number | null): string { function formatLastSeen(iso: string): string { try { const d = new Date(iso); - const now = Date.now(); - const diffMs = now - d.getTime(); + const diffMs = Date.now() - d.getTime(); if (diffMs < 60_000) return "just now"; if (diffMs < 3_600_000) return `${Math.floor(diffMs / 60_000)}m ago`; if (diffMs < 86_400_000) return `${Math.floor(diffMs / 3_600_000)}h ago`; @@ -35,21 +34,21 @@ export function NodeCard({ node, onClick }: NodeCardProps) {
onClick?.(node)} style={{ - background: "var(--card-bg, #1e1e2e)", - border: "1px solid var(--border, #2e2e3e)", - borderRadius: "8px", - padding: "16px", + background: "var(--bg-elevated)", + border: "1px solid var(--border)", + borderRadius: 8, + padding: "var(--space-4)", cursor: onClick ? "pointer" : "default", opacity: isOnline ? 1 : 0.6, - transition: "border-color 0.15s, box-shadow 0.15s", + transition: "border-color 0.15s, background 0.15s", }} onMouseEnter={(e) => { - e.currentTarget.style.borderColor = "var(--accent, #6366f1)"; - e.currentTarget.style.boxShadow = "0 0 0 1px var(--accent, #6366f1)"; + e.currentTarget.style.borderColor = "var(--accent)"; + e.currentTarget.style.background = "var(--bg-hover)"; }} onMouseLeave={(e) => { - e.currentTarget.style.borderColor = "var(--border, #2e2e3e)"; - e.currentTarget.style.boxShadow = "none"; + e.currentTarget.style.borderColor = "var(--border)"; + e.currentTarget.style.background = "var(--bg-elevated)"; }} > {/* Header */} @@ -58,25 +57,26 @@ export function NodeCard({ node, onClick }: NodeCardProps) { display: "flex", justifyContent: "space-between", alignItems: "flex-start", - marginBottom: "12px", + marginBottom: "var(--space-3)", }} >
{node.friendly_name || node.hostname || `Node ${node.node_id}`}
{node.ip} @@ -90,12 +90,12 @@ export function NodeCard({ node, onClick }: NodeCardProps) { style={{ display: "grid", gridTemplateColumns: "1fr 1fr", - gap: "8px 16px", - fontSize: "12px", + gap: "var(--space-2) var(--space-4)", + fontSize: 12, }} > - + - +
@@ -130,20 +131,21 @@ function DetailRow({
{label}
= { - online: { - bg: "rgba(34, 197, 94, 0.15)", - text: "#22c55e", - label: "Online", - }, - offline: { - bg: "rgba(239, 68, 68, 0.15)", - text: "#ef4444", - label: "Offline", - }, - degraded: { - bg: "rgba(234, 179, 8, 0.15)", - text: "#eab308", - label: "Degraded", - }, - unknown: { - bg: "rgba(148, 163, 184, 0.15)", - text: "#94a3b8", - label: "Unknown", - }, +const STATUS_STYLES: Record = { + online: { color: "var(--status-online)", label: "Online" }, + offline: { color: "var(--status-error)", label: "Offline" }, + degraded: { color: "var(--status-warning)", label: "Degraded" }, + unknown: { color: "var(--text-muted)", label: "Unknown" }, }; -const SIZE_STYLES: Record = { - sm: { fontSize: "11px", padding: "2px 8px", dot: "6px" }, - md: { fontSize: "13px", padding: "4px 12px", dot: "8px" }, - lg: { fontSize: "15px", padding: "6px 16px", dot: "10px" }, +const SIZE_STYLES: Record = { + sm: { fontSize: 11, padding: "2px 8px", dot: 6 }, + md: { fontSize: 13, padding: "4px 12px", dot: 8 }, + lg: { fontSize: 15, padding: "6px 16px", dot: 10 }, }; export function StatusBadge({ status, size = "sm" }: StatusBadgeProps) { - const style = STATUS_STYLES[status]; - const sizeStyle = SIZE_STYLES[size]; + const { color, label } = STATUS_STYLES[status]; + const s = SIZE_STYLES[size]; return ( - {style.label} + {label} ); } diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/design-system.css b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/design-system.css new file mode 100644 index 00000000..36d57caa --- /dev/null +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/design-system.css @@ -0,0 +1,152 @@ +/* + * RuView Design System (ADR-053) + * Dark professional + Unity-inspired interface + */ + +/* ===== Design Tokens ===== */ +:root { + /* Background layers */ + --bg-base: #0d1117; + --bg-surface: #161b22; + --bg-elevated: #1c2333; + --bg-hover: #242d3d; + --bg-active: #2d3748; + + /* Text hierarchy */ + --text-primary: #e6edf3; + --text-secondary: #8b949e; + --text-muted: #484f58; + + /* Status indicators */ + --status-online: #3fb950; + --status-warning: #d29922; + --status-error: #f85149; + --status-info: #58a6ff; + + /* Accent */ + --accent: #7c3aed; + --accent-hover: #6d28d9; + + /* Borders */ + --border: #30363d; + --border-active: #58a6ff; + + /* Fonts */ + --font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + + /* Spacing (4px base grid) */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 24px; + --space-6: 32px; + --space-8: 48px; + + /* Panel dimensions */ + --sidebar-width: 220px; + --sidebar-collapsed: 52px; + --statusbar-height: 28px; + --toolbar-height: 44px; +} + +/* ===== Reset ===== */ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body, #root { + height: 100%; +} + +body { + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.6; + background: var(--bg-base); + color: var(--text-primary); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ===== Typography Scale ===== */ +.heading-xl { font: 600 28px/1.2 var(--font-sans); color: var(--text-primary); } +.heading-lg { font: 600 20px/1.3 var(--font-sans); color: var(--text-primary); } +.heading-md { font: 600 16px/1.4 var(--font-sans); color: var(--text-primary); } +.heading-sm { font: 600 13px/1.4 var(--font-sans); color: var(--text-secondary); } +.body { font: 400 14px/1.6 var(--font-sans); color: var(--text-primary); } +.body-sm { font: 400 12px/1.5 var(--font-sans); color: var(--text-secondary); } +.data { font: 400 13px/1.4 var(--font-mono); color: var(--text-secondary); } +.data-lg { font: 500 18px/1.2 var(--font-mono); color: var(--text-primary); } + +/* ===== Scrollbar ===== */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-track { + background: var(--bg-base); +} +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--bg-active); +} + +/* ===== Form Controls ===== */ +input, select, textarea { + font-family: var(--font-sans); + font-size: 13px; + color: var(--text-primary); + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: 6px; + padding: var(--space-2) var(--space-3); + outline: none; + width: 100%; + box-sizing: border-box; + transition: border-color 0.15s; +} +input:focus, select:focus, textarea:focus { + border-color: var(--accent); +} +input:disabled, select:disabled, textarea:disabled { + opacity: 0.5; + cursor: not-allowed; +} +input[type="number"] { + font-family: var(--font-mono); +} +select { + cursor: pointer; +} + +/* ===== Buttons ===== */ +button { + font-family: var(--font-sans); + cursor: pointer; + border: none; + outline: none; + transition: background 0.15s, opacity 0.15s; +} +button:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +/* ===== Animations ===== */ +@keyframes pulse-accent { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +/* ===== Selection ===== */ +::selection { + background: rgba(124, 58, 237, 0.3); + color: var(--text-primary); +} diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/main.tsx b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/main.tsx index f46c379c..5e06d711 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/main.tsx +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/main.tsx @@ -1,5 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import "./design-system.css"; import App from "./App"; ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Dashboard.tsx b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Dashboard.tsx index 7bddfa57..87fcedaf 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Dashboard.tsx +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Dashboard.tsx @@ -1,4 +1,6 @@ import React, { useEffect, useState } from "react"; +import { StatusBadge } from "../components/StatusBadge"; +import type { HealthStatus } from "../types"; interface DiscoveredNode { ip: string; @@ -6,7 +8,7 @@ interface DiscoveredNode { hostname: string | null; node_id: number; firmware_version: string | null; - health: string; + health: HealthStatus; last_seen: string; } @@ -52,27 +54,29 @@ const Dashboard: React.FC = () => { fetchServerStatus(); }, []); + const onlineCount = nodes.filter((n) => n.health === "online").length; + return ( -
+
+ {/* Header */}
-

Dashboard

+

Dashboard

+ {/* Stats row */} +
+ + + + +
+ {/* Server status panel */}
-

+

Sensing Server

-
+
- + {serverStatus?.running ? `Running (PID ${serverStatus.pid})` : "Stopped"} + {serverStatus?.running && serverStatus.http_port && ( + + :{serverStatus.http_port} + + )}
- {/* Node grid */} + {/* Node list */}

Discovered Nodes ({nodes.length}) @@ -127,12 +160,13 @@ const Dashboard: React.FC = () => { {nodes.length === 0 ? (
No nodes discovered. Click "Scan Network" to search. @@ -142,55 +176,64 @@ const Dashboard: React.FC = () => { style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", - gap: 16, + gap: "var(--space-4)", }} > {nodes.map((node, i) => (
+ (e.currentTarget.style.borderColor = "var(--bg-active)") + } + onMouseLeave={(e) => + (e.currentTarget.style.borderColor = "var(--border)") + } >
-
+
{node.hostname || `Node ${node.node_id}`}
-
+
{node.ip}
- - {node.health} - +
-
-
MAC: {node.mac || "unknown"}
-
Firmware: {node.firmware_version || "unknown"}
-
Node ID: {node.node_id}
+
+
+ MAC + {node.mac || "--"} +
+
+ Firmware + {node.firmware_version || "--"} +
+
+ Node ID + {node.node_id} +
))} @@ -200,4 +243,44 @@ const Dashboard: React.FC = () => { ); }; +function StatCard({ + label, + value, + color, +}: { + label: string; + value: string; + color?: string; +}) { + return ( +
+
+ {label} +
+
+ {value} +
+
+ ); +} + export default Dashboard; diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/FlashFirmware.tsx b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/FlashFirmware.tsx index b368b33f..b1043e7f 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/FlashFirmware.tsx +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/FlashFirmware.tsx @@ -6,32 +6,25 @@ import type { SerialPort, Chip, FlashProgress, FlashPhase } from "../types"; type WizardStep = 1 | 2 | 3; export function FlashFirmware() { - // --- State --- const [step, setStep] = useState(1); const [ports, setPorts] = useState([]); - const [selectedPort, setSelectedPort] = useState(""); - const [firmwarePath, setFirmwarePath] = useState(""); + const [selectedPort, setSelectedPort] = useState(""); + const [firmwarePath, setFirmwarePath] = useState(""); const [chip, setChip] = useState("esp32s3"); - const [baud, setBaud] = useState(460800); + const [baud, setBaud] = useState(460800); const [isLoadingPorts, setIsLoadingPorts] = useState(false); const [progress, setProgress] = useState(null); const [isFlashing, setIsFlashing] = useState(false); - const [flashResult, setFlashResult] = useState<{ - success: boolean; - message: string; - } | null>(null); + const [flashResult, setFlashResult] = useState<{ success: boolean; message: string } | null>(null); const [error, setError] = useState(null); - // --- Load serial ports --- const loadPorts = useCallback(async () => { setIsLoadingPorts(true); setError(null); try { const result = await invoke("list_serial_ports"); setPorts(result); - if (result.length === 1) { - setSelectedPort(result[0].name); - } + if (result.length === 1) setSelectedPort(result[0].name); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { @@ -39,26 +32,16 @@ export function FlashFirmware() { } }, []); - useEffect(() => { - loadPorts(); - }, [loadPorts]); + useEffect(() => { loadPorts(); }, [loadPorts]); - // --- Listen for flash progress events --- useEffect(() => { let unlisten: (() => void) | undefined; - listen("flash-progress", (event) => { setProgress(event.payload); - }).then((fn) => { - unlisten = fn; - }); - - return () => { - unlisten?.(); - }; + }).then((fn) => { unlisten = fn; }); + return () => { unlisten?.(); }; }, []); - // --- File picker --- const pickFirmware = async () => { try { const { open } = await import("@tauri-apps/plugin-dialog"); @@ -69,43 +52,28 @@ export function FlashFirmware() { { name: "All Files", extensions: ["*"] }, ], }); - if (selected && typeof selected === "string") { - setFirmwarePath(selected); - } + if (selected && typeof selected === "string") setFirmwarePath(selected); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }; - // --- Flash --- const startFlash = async () => { if (!selectedPort || !firmwarePath) return; - setIsFlashing(true); setFlashResult(null); setProgress(null); setError(null); - try { - await invoke("flash_firmware", { - port: selectedPort, - firmwarePath, - chip, - baud, - }); - setFlashResult({ - success: true, - message: "Firmware flashed successfully.", - }); + await invoke("flash_firmware", { port: selectedPort, firmwarePath, chip, baud }); + setFlashResult({ success: true, message: "Firmware flashed successfully." }); } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - setFlashResult({ success: false, message: msg }); + setFlashResult({ success: false, message: err instanceof Error ? err.message : String(err) }); } finally { setIsFlashing(false); } }; - // --- Step validation --- const canProceed = (s: WizardStep): boolean => { if (s === 1) return selectedPort !== ""; if (s === 2) return firmwarePath !== ""; @@ -113,43 +81,16 @@ export function FlashFirmware() { }; return ( -
-

- Flash Firmware -

-

+

+

Flash Firmware

+

Flash firmware to an ESP32 via serial connection

- {/* Step indicator */} - {/* Error banner */} {error && ( -
+
{error}
)} @@ -157,47 +98,33 @@ export function FlashFirmware() { {/* Step 1: Select Serial Port */} {step === 1 && (
-

Step 1: Select Serial Port

-

- Connect your ESP32 via USB and select the serial port. -

+

Step 1: Select Serial Port

+

Connect your ESP32 via USB and select the serial port.

-
+
-
+
- +
-
@@ -207,42 +134,21 @@ export function FlashFirmware() { {/* Step 2: Select Firmware */} {step === 2 && (
-

Step 2: Select Firmware

-

- Choose the firmware binary file and chip configuration. -

+

Step 2: Select Firmware

+

Choose the firmware binary file and chip configuration.

-
+
-
- - +
+ +
-
+
- setChip(e.target.value as Chip)}> @@ -250,11 +156,7 @@ export function FlashFirmware() {
- setBaud(Number(e.target.value))}> @@ -264,14 +166,8 @@ export function FlashFirmware() {
- - +
@@ -281,87 +177,58 @@ export function FlashFirmware() { {/* Step 3: Flash */} {step === 3 && (
-

Step 3: Flash

+

Step 3: Flash

{/* Summary */}
- +
{/* Progress */} {(isFlashing || progress) && !flashResult && ( -
+
)} {/* Result */} {flashResult && ( -
+
{flashResult.message}
)}
{flashResult ? ( ) : ( - )} @@ -372,9 +239,7 @@ export function FlashFirmware() { ); } -// --------------------------------------------------------------------------- -// Sub-components -// --------------------------------------------------------------------------- +// --- Sub-components --- function StepIndicator({ current }: { current: WizardStep }) { const steps = [ @@ -384,65 +249,42 @@ function StepIndicator({ current }: { current: WizardStep }) { ]; return ( -
+
{steps.map(({ n, label }, i) => { const isActive = n === current; const isDone = n < current; return (
-
+
{isDone ? "\u2713" : n}
{label}
{i < steps.length - 1 && ( -
+
)}
); @@ -468,42 +310,21 @@ function ProgressBar({ progress }: { progress: FlashProgress | null }) { return (
-
- - {PHASE_LABELS[phase]} - - - {pct.toFixed(1)}% {speedKB && `| ${speedKB}`} +
+ {PHASE_LABELS[phase]} + + {pct.toFixed(1)}%{speedKB && ` | ${speedKB}`}
-
+
@@ -514,102 +335,81 @@ function ProgressBar({ progress }: { progress: FlashProgress | null }) { function SummaryField({ label, value }: { label: string; value: string }) { return (
-
+
{label}
-
+
{value}
); } -// --------------------------------------------------------------------------- -// Shared styles -// --------------------------------------------------------------------------- +// --- Shared styles --- + +function bannerStyle(color: string): React.CSSProperties { + return { + background: `color-mix(in srgb, ${color} 10%, transparent)`, + border: `1px solid color-mix(in srgb, ${color} 30%, transparent)`, + borderRadius: 6, + padding: "var(--space-3) var(--space-4)", + marginBottom: "var(--space-4)", + fontSize: 13, + color, + }; +} const cardStyle: React.CSSProperties = { - background: "var(--card-bg, #1e1e2e)", - border: "1px solid var(--border, #2e2e3e)", - borderRadius: "8px", - padding: "20px", + background: "var(--bg-surface)", + border: "1px solid var(--border)", + borderRadius: 8, + padding: "var(--space-5)", }; -const stepTitle: React.CSSProperties = { - fontSize: "16px", +const stepTitleStyle: React.CSSProperties = { + fontSize: 16, fontWeight: 600, - color: "var(--text-primary, #e2e8f0)", - margin: "0 0 4px", + color: "var(--text-primary)", + margin: "0 0 var(--space-1)", + fontFamily: "var(--font-sans)", }; -const stepDesc: React.CSSProperties = { - fontSize: "13px", - color: "var(--text-secondary, #94a3b8)", - marginBottom: "16px", +const stepDescStyle: React.CSSProperties = { + fontSize: 13, + color: "var(--text-secondary)", + marginBottom: "var(--space-4)", }; const labelStyle: React.CSSProperties = { display: "block", - fontSize: "12px", + fontSize: 12, fontWeight: 600, - color: "var(--text-secondary, #94a3b8)", - marginBottom: "6px", + color: "var(--text-secondary)", + marginBottom: 6, + fontFamily: "var(--font-sans)", }; -const inputStyle: React.CSSProperties = { - width: "100%", - padding: "8px 12px", - border: "1px solid var(--border, #2e2e3e)", - borderRadius: "6px", - background: "var(--input-bg, #12121a)", - color: "var(--text-primary, #e2e8f0)", - fontSize: "13px", - outline: "none", - boxSizing: "border-box", -}; - -const primaryBtnStyle: React.CSSProperties = { - padding: "8px 20px", - border: "none", - borderRadius: "6px", - background: "var(--accent, #6366f1)", +const primaryBtn: React.CSSProperties = { + padding: "var(--space-2) 20px", + borderRadius: 6, + background: "var(--accent)", color: "#fff", - fontSize: "13px", + fontSize: 13, fontWeight: 600, - cursor: "pointer", }; -const secondaryBtnStyle: React.CSSProperties = { - padding: "8px 16px", - border: "1px solid var(--border, #2e2e3e)", - borderRadius: "6px", +const secondaryBtn: React.CSSProperties = { + padding: "var(--space-2) var(--space-4)", + border: "1px solid var(--border)", + borderRadius: 6, background: "transparent", - color: "var(--text-secondary, #94a3b8)", - fontSize: "13px", + color: "var(--text-secondary)", + fontSize: 13, fontWeight: 500, - cursor: "pointer", }; -const disabledBtnStyle: React.CSSProperties = { - ...primaryBtnStyle, - background: "var(--border, #2e2e3e)", - color: "var(--text-muted, #64748b)", - cursor: "not-allowed", +const disabledBtn: React.CSSProperties = { + ...primaryBtn, + background: "var(--bg-active)", + color: "var(--text-muted)", }; diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Nodes.tsx b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Nodes.tsx index 64f4c8fa..df26fda0 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Nodes.tsx +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Nodes.tsx @@ -16,34 +16,19 @@ export function Nodes() { }; return ( -
+
{/* Header */}
-

- Nodes -

-

+

Nodes

+

{nodes.length} node{nodes.length !== 1 ? "s" : ""} in registry

@@ -51,16 +36,12 @@ export function Nodes() { onClick={scan} disabled={isScanning} style={{ - padding: "8px 16px", - border: "none", - borderRadius: "6px", - background: isScanning - ? "var(--border, #2e2e3e)" - : "var(--accent, #6366f1)", - color: isScanning ? "var(--text-muted, #64748b)" : "#fff", - fontSize: "13px", + padding: "var(--space-2) var(--space-4)", + borderRadius: 6, + background: isScanning ? "var(--bg-active)" : "var(--accent)", + color: isScanning ? "var(--text-muted)" : "#fff", + fontSize: 13, fontWeight: 600, - cursor: isScanning ? "not-allowed" : "pointer", }} > {isScanning ? "Scanning..." : "Refresh"} @@ -71,13 +52,13 @@ export function Nodes() { {error && (
{error} @@ -88,13 +69,13 @@ export function Nodes() { {nodes.length === 0 ? (
{isScanning ? "Scanning for nodes..." : "No nodes found. Run a scan to discover ESP32 devices."} @@ -102,26 +83,15 @@ export function Nodes() { ) : (
- +
- + @@ -133,12 +103,11 @@ export function Nodes() { {nodes.map((node) => { const key = node.mac ?? node.ip; - const isExpanded = expandedMac === key; return ( toggleExpand(node)} /> ); @@ -151,20 +120,17 @@ export function Nodes() { ); } -// --------------------------------------------------------------------------- -// Sub-components -// --------------------------------------------------------------------------- - function Th({ children }: { children: React.ReactNode }) { return ( - (e.currentTarget.style.background = "rgba(255,255,255,0.02)") - } - onMouseLeave={(e) => - (e.currentTarget.style.background = "transparent") - } + onMouseEnter={(e) => (e.currentTarget.style.background = "var(--bg-hover)")} + onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")} > - + - + {isExpanded && ( - - + @@ -255,17 +210,17 @@ function ExpandedDetails({ node }: { node: Node }) { return (
- + - {node.friendly_name && ( - - )} + {node.friendly_name && } {node.notes && }
); } -function DetailField({ label, value }: { label: string; value: string }) { +function DetailField({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) { return (
{label}
-
{value}
+
+ {value} +
); } diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Settings.tsx b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Settings.tsx index ff181470..891d9efc 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Settings.tsx +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui/src/pages/Settings.tsx @@ -5,7 +5,7 @@ const DEFAULT_SETTINGS: AppSettings = { server_http_port: 8080, server_ws_port: 8765, server_udp_port: 5005, - bind_address: "0.0.0.0", + bind_address: "127.0.0.1", ui_path: "", ota_psk: "", auto_discover: true, @@ -19,17 +19,14 @@ export function Settings() { const [showPsk, setShowPsk] = useState(false); const [error, setError] = useState(null); - // Load persisted settings on mount useEffect(() => { (async () => { try { const { invoke } = await import("@tauri-apps/api/core"); const persisted = await invoke("get_settings"); - if (persisted) { - setSettings(persisted); - } + if (persisted) setSettings(persisted); } catch { - // Settings command may not exist yet -- use defaults + // Settings command may not exist yet } })(); }, []); @@ -60,55 +57,38 @@ export function Settings() { }; return ( -
-

- Settings -

-

+

+

Settings

+

Configure server, network, and application preferences

- {/* Error */} {error && (
{error}
)} - {/* Saved toast */} {saved && (
Settings saved. @@ -117,50 +97,32 @@ export function Settings() { {/* Sensing Server */}
-
+
- update("server_http_port", v)} - min={1} - max={65535} - /> + update("server_http_port", v)} min={1} max={65535} /> - update("server_ws_port", v)} - min={1} - max={65535} - /> + update("server_ws_port", v)} min={1} max={65535} /> - update("server_udp_port", v)} - min={1} - max={65535} - /> + update("server_udp_port", v)} min={1} max={65535} /> - update("bind_address", v)} - placeholder="0.0.0.0" + onChange={(e) => update("bind_address", e.target.value)} + placeholder="127.0.0.1" + style={{ fontFamily: "var(--font-mono)" }} />
-
+
- update("ui_path", v)} + onChange={(e) => update("ui_path", e.target.value)} placeholder="Leave empty for default" /> @@ -170,28 +132,19 @@ export function Settings() { {/* Security */}
-
+
update("ota_psk", e.target.value)} placeholder="Enter PSK for OTA authentication" - style={{ ...inputStyle, flex: 1 }} + style={{ flex: 1, fontFamily: "var(--font-mono)" }} /> -
-

+

Used for authenticating OTA firmware updates to nodes.

@@ -199,36 +152,16 @@ export function Settings() { {/* Discovery */}
-
+
- @@ -245,51 +178,34 @@ export function Settings() {
{/* Actions */} -
- - +
+ +
); } -// --------------------------------------------------------------------------- -// Sub-components -// --------------------------------------------------------------------------- +// --- Sub-components --- -function Section({ - title, - children, -}: { - title: string; - children: React.ReactNode; -}) { +function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title} @@ -299,22 +215,17 @@ function Section({ ); } -function Field({ - label, - children, -}: { - label: string; - children: React.ReactNode; -}) { +function Field({ label, children }: { label: string; children: React.ReactNode }) { return (

Status MAC IP
{children} @@ -172,20 +138,15 @@ function Th({ children }: { children: React.ReactNode }) { ); } -function Td({ - children, - mono = false, -}: { - children: React.ReactNode; - mono?: boolean; -}) { +function Td({ children, mono = false }: { children: React.ReactNode; mono?: boolean }) { return ( {children} @@ -220,29 +181,23 @@ function NodeRow({
- - {node.mac ?? "--"} {node.ip}{node.firmware_version ?? "--"}{node.firmware_version ?? "--"} {node.chip.toUpperCase()} {formatLastSeen(node.last_seen)}
+