diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx
index 56876d9..1cf3cbf 100644
--- a/plugins/decky-vault/src/components/main-panel.tsx
+++ b/plugins/decky-vault/src/components/main-panel.tsx
@@ -21,6 +21,10 @@ import {
FaSearch,
FaQrcode,
FaLink,
+ FaMicrochip,
+ FaGamepad,
+ FaServer,
+ FaWrench,
} from "react-icons/fa"
import type { RecordingState, SessionData, RecentSession, PluginSettings } from "../lib/store"
import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared"
@@ -58,16 +62,85 @@ const HARDWARE_OPTIONS = [
...KNOWN_HARDWARE_SLUGS.map((slug) => ({ label: slug, data: slug })),
]
+// ── Reusable layout helpers ─────────────────────────────────────
+// Small, consistent building blocks so every section looks uniform.
+
+const COLORS = {
+ ok: "#2ecc71",
+ err: "#e74c3c",
+ accent: "#1b9bf3",
+ muted: "rgba(255,255,255,0.55)",
+ panel: "rgba(255,255,255,0.06)",
+ border: "rgba(255,255,255,0.10)",
+}
+
+/** Muted intro line shown directly under a PanelSection title. */
+function SectionHint({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+/** A labelled status row with a colored icon: ✓/✗ + label + value. */
+function StatusRow({
+ ok,
+ label,
+ value,
+}: {
+ ok: boolean | null
+ label: string
+ value?: string
+}) {
+ const icon = ok === null ? null : ok ? :
+ return (
+
+
+ {icon}
+ {label}
+ {value && {value}}
+
+
+ )
+}
+
+/** A labelled key/value row used for system info (Game, App ID, etc). */
+function InfoRow({ icon, label, value }: { icon: React.ReactNode; label: string; value: React.ReactNode }) {
+ return (
+
+
+ {icon}
+ {label}
+ {value}
+
+
+ )
+}
+
+/** Standard centered icon+label content for ButtonItem. */
+function BtnContent({ icon, label }: { icon: React.ReactNode; label: string }) {
+ return (
+
+ {icon}
+ {label}
+
+ )
+}
+
+/** A numbered setup step with a circular badge. */
function SetupStep({ number, title, body }: { number: number; title: string; body: string }) {
return (
-
+
void }) {
+ return (
+ <>
+
+
+ {value}
+
+
+
+
+ } label={copied ? "Copied to clipboard" : "Copy"} />
+
+
+ >
+ )
+}
+
export default function MainPanel({
recordingState,
session,
@@ -197,6 +297,10 @@ export default function MainPanel({
async function handleWriteConfig() {
const result = await writeMangohudConfig()
setConfigWritten(result.success)
+ if (result.success) {
+ // Reset verify state since config changed
+ setConfigVerified({ checked: false, valid: false, message: "" })
+ }
}
async function handleVerifyConfig() {
@@ -208,7 +312,6 @@ export default function MainPanel({
const content = result.content
const hasOutputFolder = content.includes("output_folder=/tmp")
const hasFps = content.includes("fps")
- const hasFrameTiming = content.includes("frame_timing")
if (hasOutputFolder && hasFps) {
setConfigVerified({ checked: true, valid: true, message: "Config looks good" })
} else {
@@ -303,6 +406,10 @@ export default function MainPanel({
return () => stopPairPolling()
}, [])
+ // ── Derived readiness flags ─────────────────────────────
+ const hasApiKey = !!settings.apiKey
+ const isLinked = pairState.status === "linked" || hasApiKey
+
// ── Stopped state: show the session form ──────────────────────
if (recordingState === "stopped") {
return (
@@ -320,16 +427,30 @@ export default function MainPanel({
return (
<>
- {/* ── Recording ──────────────────────────────────────────── */}
+ {/* ════════════════════════════════════════════════════════
+ 1. RECORDING — the primary action, always first
+ ════════════════════════════════════════════════════════ */}
{error && (
- {error}
+
+ {error}
+
)}
{recordingState === "idle" && (
<>
+
+ Enter the game name (or let it auto-detect), then start recording once you're in-game.
+
-
-
- Start Recording
-
+ } label="Start Recording" />
>
@@ -352,112 +470,109 @@ export default function MainPanel({
{recordingState === "recording" && (
<>
-
-
-
-
{formatTime(elapsed)}
+
+
+
+ {formatTime(elapsed)}
- {session.gameName ? `Recording: ${session.gameName}` : "Recording..."}
+ {session.gameName ? session.gameName : "Recording…"}
-
-
- Stop Recording
-
+ } label="Stop Recording" />
>
)}
{recentSessions.length > 0 && recordingState === "idle" && (
-
+ <>
+
+
+ Recent
+
+
{recentSessions.map((rs, i) => (
-
-
{rs.gameName || "Unknown game"}
-
-
+
+
{rs.gameName || "Unknown game"}
+
{rs.fpsAvg ? `${rs.fpsAvg} FPS avg` : "No data"} · {new Date(rs.date).toLocaleDateString()}
-
+
))}
-
+ >
)}
- {/* ── Status ──────────────────────────────────────────────── */}
-
-
-
-
-
- Check MangoHud Status
-
-
-
- {mangohudStatus.checked && (
-
-
- {mangohudStatus.installed ? (
- <> MangoHud {mangohudStatus.version}>
- ) : (
- <> MangoHud not found>
- )}
-
-
- )}
- {session.gameName && (
-
-
- Game: {session.gameName}
- {session.appId && <> App ID: {session.appId}>}
-
-
- )}
-
-
- {keyTestStatus === "testing" ? "Testing..." : "Test API Key"}
- {keyTestStatus === "valid" && }
- {keyTestStatus === "invalid" && }
-
-
- {keyTestMessage && (
-
-
- {keyTestMessage}
-
-
- )}
-
-
- {/* ── Account ─────────────────────────────────────────── */}
+ {/* ════════════════════════════════════════════════════════
+ 2. ACCOUNT — pairing + API key (prerequisite for upload)
+ ════════════════════════════════════════════════════════ */}
+ {/* Readiness indicator */}
+
+
{pairState.status === "idle" && (
<>
+
+ Scan a QR code with your phone to link your DeckyVault account — no manual key entry needed.
+
-
- Link this plugin to your DeckyVault account by scanning a QR code with your phone — no manual key entry needed.
+
+ } label="Pair with Phone" />
+
+
+ {/* Manual key entry + test */}
+
+
+ Or paste a key manually
-
-
-
- Pair with Phone
-
+ onUpdateSetting("apiKey", e.target.value)}
+ placeholder="dv_..."
+ bIsPassword
+ />
+
+
+
+ : keyTestStatus === "valid" ? : keyTestStatus === "invalid" ? : }
+ label={keyTestStatus === "testing" ? "Testing…" : "Test API Key"}
+ />
+ {keyTestMessage && (
+
+
+ {keyTestMessage}
+
+
+ )}
>
)}
{pairState.status === "starting" && (
-
+
Starting pairing session…
@@ -465,13 +580,9 @@ export default function MainPanel({
{(pairState.status === "showing-qr" || pairState.status === "polling") && (
<>
+
Scan with your phone camera, then tap Confirm on the page.
-
- Scan this code with your phone's camera, then confirm on the page that opens.
-
-
-
-
+
@@ -482,10 +593,7 @@ export default function MainPanel({
-
-
- Cancel
-
+ } label="Cancel" />
>
@@ -494,21 +602,26 @@ export default function MainPanel({
{pairState.status === "linked" && (
<>
-
-
Plugin linked to your account!
+
+ Linked to your account
-
- API key saved. You can now upload performance entries.
+
+ API key saved. You can now upload entries.
-
-
- Done
-
+ } label="Done" />
>
@@ -517,106 +630,122 @@ export default function MainPanel({
{pairState.status === "error" && (
<>
-
-
{pairState.error}
+
+ {pairState.error}
-
-
- Try Again
-
+ } label="Try Again" />
-
- Dismiss
-
+ } label="Dismiss" />
>
)}
- {/* ── Usage Instructions ──────────────────────────────────── */}
-
-
-
- Add this to your game's Steam launch options, then launch the game. Press Start Recording once you're in-game and ready to benchmark.
-
-
-
-
- ~/deckyvault-mangohud.sh %command%
-
-
-
-
-
-
- {copiedLaunchOpt ? "Copied to clipboard" : "Copy Launch Option"}
-
-
-
-
-
- Config stored in ~/.config/MangoHud/MangoHud.conf
-
-
-
+ {/* ════════════════════════════════════════════════════════
+ 3. MANGOHUD SETUP — config + launch option + guide,
+ all in one coherent section
+ ════════════════════════════════════════════════════════ */}
+
+ Follow these steps once to enable performance logging.
- {/* ── MangoHud Config ────────────────────────────────────── */}
-
+
-
-
- Write Config
-
+ } label="Write Config" />
{configWritten && (
-
-
- Config written
-
-
+
)}
+
+
+
+
+
+
+ {/* Verify + config details */}
+
+
+ Verify
+
+
-
-
- Verify Config
-
+ } label="Verify Config" />
{configVerified.checked && (
-
-
- {configVerified.valid ? : } {configVerified.message}
-
-
+
+ )}
+
+
+
+ Config stored in ~/.config/MangoHud/MangoHud.conf.
+ Steam Deck has MangoHud pre-installed; on other Linux, install via apt or Flatpak.
+
+
+
+
+ {/* ════════════════════════════════════════════════════════
+ 4. SYSTEM — environment / detection status
+ ════════════════════════════════════════════════════════ */}
+
+ Detect your hardware, OS, and the currently running game.
+
+
+
+ } label="Check MangoHud" />
+
+
+ {mangohudStatus.checked && (
+
+ )}
+
+ {session.gameName && (
+ } label="Game" value={session.gameName} />
+ )}
+ {session.appId && (
+ } label="App ID" value={String(session.appId)} />
)}
- {/* ── Configuration ────────────────────────────────────────── */}
-
+ {/* ════════════════════════════════════════════════════════
+ 5. ADVANCED — server, paths, hardware, config portability
+ ════════════════════════════════════════════════════════ */}
+
+ Change these only if you need a custom server or export location.
+
onUpdateSetting("apiKey", e.target.value)}
- placeholder="dv_..."
- bIsPassword
+ label="Server URL"
+ value={settings.baseUrl}
+ onChange={(e) => onUpdateSetting("baseUrl", e.target.value)}
+ placeholder="https://deckyvault.xyz"
/>
@@ -627,14 +756,6 @@ export default function MainPanel({
placeholder="/home/deck/Downloads"
/>
-
- onUpdateSetting("baseUrl", e.target.value)}
- placeholder="https://deckyvault.xyz"
- />
-
onUpdateSetting("hardwareSlug", opt.data as string || null)}
/>
+
+
+
+
+ Config Backup
+
+
-
-
- Export Config to Downloads
-
+ } label="Export to Downloads" />
-
-
- Import Config from Downloads
-
+ } label="Import from Downloads" />
{configStatus && (
-
+
{configStatus.isError ? : } {configStatus.message}
)}
-
-
- {/* ── MangoHud Setup Guide ────────────────────────────── */}
-
-
-
- Follow these steps once to enable performance logging.
-
-
-
-
-
-
-
-
- Steam Deck ships with MangoHud pre-installed. On other Linux distros, install it with
sudo apt install mangohud or via Flatpak.
+
+
+ Export saves your settings (including API key) as a JSON file you can move between devices.