refactor: migrate UnlockLayout to Tailwind + extract ToolShell wrapper
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m54s

- Convert all ~40 inline styles in UnlockLayout.vue to Tailwind CSS v4 arbitrary-value classes, matching MainLayout.vue color conventions (CSS variables + hex arbitraries). Visual appearance preserved exactly.
- Create ToolShell.vue reusable wrapper that owns output/running state and execute/setOutput API via defineExpose.
- Refactor PingTool, TracerouteTool, DnsLookup, WhoisTool, BandwidthTest to use ToolShell — each tool now contains only its unique inputs and invoke calls. Zero vue-tsc errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-29 16:53:57 -04:00
parent b86e2d68d8
commit d4bfb3d5fd
7 changed files with 105 additions and 189 deletions

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="flex flex-col h-full p-4 gap-3"> <ToolShell ref="shell" placeholder="Select a mode and click Run Test">
<div class="flex items-center gap-2"> <template #default="{ running }">
<select v-model="mode" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none cursor-pointer"> <select v-model="mode" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none cursor-pointer">
<option value="speedtest">Internet Speed Test</option> <option value="speedtest">Internet Speed Test</option>
<option value="iperf">iperf3 (LAN)</option> <option value="iperf">iperf3 (LAN)</option>
@ -13,32 +13,31 @@
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="run"> <button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="run">
{{ running ? "Testing..." : "Run Test" }} {{ running ? "Testing..." : "Run Test" }}
</button> </button>
</div> </template>
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Select a mode and click Run Test" }}</pre> </ToolShell>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import ToolShell from "./ToolShell.vue";
const props = defineProps<{ sessionId: string }>(); const props = defineProps<{ sessionId: string }>();
const mode = ref("speedtest"); const mode = ref("speedtest");
const server = ref(""); const server = ref("");
const duration = ref(5); const duration = ref(5);
const output = ref(""); const shell = ref<InstanceType<typeof ToolShell> | null>(null);
const running = ref(false);
async function run(): Promise<void> { async function run(): Promise<void> {
running.value = true; if (mode.value === "iperf" && !server.value) {
output.value = mode.value === "iperf" ? `Running iperf3 to ${server.value}...\n` : "Running speed test...\n"; shell.value?.setOutput("Enter an iperf3 server IP");
try { return;
if (mode.value === "iperf") {
if (!server.value) { output.value = "Enter an iperf3 server IP"; running.value = false; return; }
output.value = await invoke<string>("tool_bandwidth_iperf", { sessionId: props.sessionId, server: server.value, duration: duration.value });
} else {
output.value = await invoke<string>("tool_bandwidth_speedtest", { sessionId: props.sessionId });
} }
} catch (err) { output.value = String(err); } shell.value?.execute(() => {
running.value = false; if (mode.value === "iperf") {
return invoke<string>("tool_bandwidth_iperf", { sessionId: props.sessionId, server: server.value, duration: duration.value });
}
return invoke<string>("tool_bandwidth_speedtest", { sessionId: props.sessionId });
});
} }
</script> </script>

View File

@ -1,31 +1,29 @@
<template> <template>
<div class="flex flex-col h-full p-4 gap-3"> <ToolShell ref="shell" placeholder="Enter a domain and click Lookup">
<div class="flex items-center gap-2"> <template #default="{ running }">
<input v-model="domain" type="text" placeholder="Domain name" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="lookup" /> <input v-model="domain" type="text" placeholder="Domain name" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="lookup" />
<select v-model="recordType" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none cursor-pointer"> <select v-model="recordType" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none cursor-pointer">
<option v-for="t in ['A','AAAA','MX','NS','TXT','CNAME','SOA','SRV','PTR']" :key="t" :value="t">{{ t }}</option> <option v-for="t in ['A','AAAA','MX','NS','TXT','CNAME','SOA','SRV','PTR']" :key="t" :value="t">{{ t }}</option>
</select> </select>
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="lookup">Lookup</button> <button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="lookup">Lookup</button>
</div> </template>
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a domain and click Lookup" }}</pre> </ToolShell>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import ToolShell from "./ToolShell.vue";
const props = defineProps<{ sessionId: string }>(); const props = defineProps<{ sessionId: string }>();
const domain = ref(""); const domain = ref("");
const recordType = ref("A"); const recordType = ref("A");
const output = ref(""); const shell = ref<InstanceType<typeof ToolShell> | null>(null);
const running = ref(false);
async function lookup(): Promise<void> { async function lookup(): Promise<void> {
if (!domain.value) return; if (!domain.value) return;
running.value = true; shell.value?.execute(() =>
try { invoke<string>("tool_dns_lookup", { sessionId: props.sessionId, domain: domain.value, recordType: recordType.value })
output.value = await invoke<string>("tool_dns_lookup", { sessionId: props.sessionId, domain: domain.value, recordType: recordType.value }); );
} catch (err) { output.value = String(err); }
running.value = false;
} }
</script> </script>

View File

@ -1,32 +1,28 @@
<template> <template>
<div class="flex flex-col h-full p-4 gap-3"> <ToolShell ref="shell" placeholder="Enter a host and click Ping">
<div class="flex items-center gap-2"> <template #default="{ running }">
<input v-model="target" type="text" placeholder="Host to ping" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="ping" /> <input v-model="target" type="text" placeholder="Host to ping" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="ping" />
<input v-model.number="count" type="number" min="1" max="100" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-16" /> <input v-model.number="count" type="number" min="1" max="100" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] w-16" />
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="ping">Ping</button> <button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="ping">Ping</button>
</div> </template>
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a host and click Ping" }}</pre> </ToolShell>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import ToolShell from "./ToolShell.vue";
const props = defineProps<{ sessionId: string }>(); const props = defineProps<{ sessionId: string }>();
const target = ref(""); const target = ref("");
const count = ref(4); const count = ref(4);
const output = ref(""); const shell = ref<InstanceType<typeof ToolShell> | null>(null);
const running = ref(false);
async function ping(): Promise<void> { async function ping(): Promise<void> {
if (!target.value) return; if (!target.value) return;
running.value = true; shell.value?.execute(async () => {
output.value = `Pinging ${target.value}...\n`;
try {
const result = await invoke<{ target: string; output: string }>("tool_ping", { sessionId: props.sessionId, target: target.value, count: count.value }); const result = await invoke<{ target: string; output: string }>("tool_ping", { sessionId: props.sessionId, target: target.value, count: count.value });
output.value = result.output; return result.output;
} catch (err) { output.value = String(err); } });
running.value = false;
} }
</script> </script>

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import { ref } from "vue";
defineProps<{
placeholder?: string;
}>();
const output = ref("");
const running = ref(false);
async function execute(fn: () => Promise<string>): Promise<void> {
running.value = true;
output.value = "";
try {
output.value = await fn();
} catch (err: unknown) {
output.value = `Error: ${err instanceof Error ? err.message : String(err)}`;
} finally {
running.value = false;
}
}
function setOutput(value: string): void {
output.value = value;
}
defineExpose({ execute, setOutput, output, running });
</script>
<template>
<div class="flex flex-col h-full p-4 gap-3">
<div class="flex items-center gap-2">
<slot :running="running" />
</div>
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || placeholder || "Ready." }}</pre>
</div>
</template>

View File

@ -1,29 +1,25 @@
<template> <template>
<div class="flex flex-col h-full p-4 gap-3"> <ToolShell ref="shell" placeholder="Enter a host and click Trace">
<div class="flex items-center gap-2"> <template #default="{ running }">
<input v-model="target" type="text" placeholder="Host to trace" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="trace" /> <input v-model="target" type="text" placeholder="Host to trace" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="trace" />
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="trace">Trace</button> <button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="trace">Trace</button>
</div> </template>
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a host and click Trace" }}</pre> </ToolShell>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import ToolShell from "./ToolShell.vue";
const props = defineProps<{ sessionId: string }>(); const props = defineProps<{ sessionId: string }>();
const target = ref(""); const target = ref("");
const output = ref(""); const shell = ref<InstanceType<typeof ToolShell> | null>(null);
const running = ref(false);
async function trace(): Promise<void> { async function trace(): Promise<void> {
if (!target.value) return; if (!target.value) return;
running.value = true; shell.value?.execute(() =>
output.value = `Tracing route to ${target.value}...\n`; invoke<string>("tool_traceroute", { sessionId: props.sessionId, target: target.value })
try { );
output.value = await invoke<string>("tool_traceroute", { sessionId: props.sessionId, target: target.value });
} catch (err) { output.value = String(err); }
running.value = false;
} }
</script> </script>

View File

@ -1,26 +1,25 @@
<template> <template>
<div class="flex flex-col h-full p-4 gap-3"> <ToolShell ref="shell" placeholder="Enter a domain or IP and click Whois">
<div class="flex items-center gap-2"> <template #default="{ running }">
<input v-model="target" type="text" placeholder="Domain or IP" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="lookup" /> <input v-model="target" type="text" placeholder="Domain or IP" class="px-3 py-1.5 text-sm rounded bg-[#161b22] border border-[#30363d] text-[#e0e0e0] outline-none focus:border-[#58a6ff] flex-1" @keydown.enter="lookup" />
<button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="lookup">Whois</button> <button class="px-4 py-1.5 text-sm font-bold rounded bg-[#58a6ff] text-black cursor-pointer disabled:opacity-40" :disabled="running" @click="lookup">Whois</button>
</div> </template>
<pre class="flex-1 overflow-auto bg-[#161b22] border border-[#30363d] rounded p-3 text-xs font-mono whitespace-pre-wrap text-[#e0e0e0]">{{ output || "Enter a domain or IP and click Whois" }}</pre> </ToolShell>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import ToolShell from "./ToolShell.vue";
const props = defineProps<{ sessionId: string }>(); const props = defineProps<{ sessionId: string }>();
const target = ref(""); const target = ref("");
const output = ref(""); const shell = ref<InstanceType<typeof ToolShell> | null>(null);
const running = ref(false);
async function lookup(): Promise<void> { async function lookup(): Promise<void> {
if (!target.value) return; if (!target.value) return;
running.value = true; shell.value?.execute(() =>
try { output.value = await invoke<string>("tool_whois", { sessionId: props.sessionId, target: target.value }); } invoke<string>("tool_whois", { sessionId: props.sessionId, target: target.value })
catch (err) { output.value = String(err); } );
running.value = false;
} }
</script> </script>

View File

@ -50,68 +50,25 @@ const displayError = computed(() => localError.value ?? app.error);
</script> </script>
<template> <template>
<div <div class="h-full flex items-center justify-center bg-[var(--wraith-bg-primary)]">
class="unlock-root" <div class="w-full max-w-[400px] p-10 bg-[var(--wraith-bg-secondary)] border border-[var(--wraith-border)] rounded-xl shadow-[0_8px_32px_rgba(0,0,0,0.5)]">
style="
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--wraith-bg-primary);
"
>
<div
class="unlock-card"
style="
width: 100%;
max-width: 400px;
padding: 2.5rem;
background-color: var(--wraith-bg-secondary);
border: 1px solid var(--wraith-border);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
"
>
<!-- Logo --> <!-- Logo -->
<div style="text-align: center; margin-bottom: 2rem"> <div class="text-center mb-8">
<span <span class="text-[2rem] font-extrabold tracking-[0.3em] text-[var(--wraith-accent-blue)] uppercase font-['Inter',monospace]">
style="
font-size: 2rem;
font-weight: 800;
letter-spacing: 0.3em;
color: var(--wraith-accent-blue);
text-transform: uppercase;
font-family: 'Inter', monospace;
"
>
WRAITH WRAITH
</span> </span>
<p <p class="mt-2 text-[0.8rem] text-[var(--wraith-text-muted)] tracking-[0.15em] uppercase">
style="
margin: 0.5rem 0 0;
font-size: 0.8rem;
color: var(--wraith-text-muted);
letter-spacing: 0.15em;
text-transform: uppercase;
"
>
{{ isFirstRun ? "Initialize Secure Vault" : "Secure Desktop" }} {{ isFirstRun ? "Initialize Secure Vault" : "Secure Desktop" }}
</p> </p>
</div> </div>
<!-- Form --> <!-- Form -->
<form @submit.prevent="handleSubmit" style="display: flex; flex-direction: column; gap: 1rem"> <form @submit.prevent="handleSubmit" class="flex flex-col gap-4">
<!-- Master password --> <!-- Master password -->
<div> <div>
<label <label
for="master-password" for="master-password"
style=" class="block mb-[0.4rem] text-[0.8rem] text-[var(--wraith-text-secondary)] tracking-[0.05em]"
display: block;
margin-bottom: 0.4rem;
font-size: 0.8rem;
color: var(--wraith-text-secondary);
letter-spacing: 0.05em;
"
> >
MASTER PASSWORD MASTER PASSWORD
</label> </label>
@ -122,20 +79,7 @@ const displayError = computed(() => localError.value ?? app.error);
autocomplete="current-password" autocomplete="current-password"
placeholder="Enter master password" placeholder="Enter master password"
:disabled="loading" :disabled="loading"
style=" class="w-full px-[0.9rem] py-[0.65rem] bg-[var(--wraith-bg-tertiary)] border border-[var(--wraith-border)] rounded-[6px] text-[var(--wraith-text-primary)] text-[0.95rem] outline-none transition-colors duration-150 box-border focus:border-[var(--wraith-accent-blue)]"
width: 100%;
padding: 0.65rem 0.9rem;
background-color: var(--wraith-bg-tertiary);
border: 1px solid var(--wraith-border);
border-radius: 6px;
color: var(--wraith-text-primary);
font-size: 0.95rem;
outline: none;
transition: border-color 0.15s ease;
box-sizing: border-box;
"
@focus="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-accent-blue)'"
@blur="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-border)'"
/> />
</div> </div>
@ -143,13 +87,7 @@ const displayError = computed(() => localError.value ?? app.error);
<div v-if="isFirstRun"> <div v-if="isFirstRun">
<label <label
for="confirm-password" for="confirm-password"
style=" class="block mb-[0.4rem] text-[0.8rem] text-[var(--wraith-text-secondary)] tracking-[0.05em]"
display: block;
margin-bottom: 0.4rem;
font-size: 0.8rem;
color: var(--wraith-text-secondary);
letter-spacing: 0.05em;
"
> >
CONFIRM PASSWORD CONFIRM PASSWORD
</label> </label>
@ -160,28 +98,9 @@ const displayError = computed(() => localError.value ?? app.error);
autocomplete="new-password" autocomplete="new-password"
placeholder="Confirm master password" placeholder="Confirm master password"
:disabled="loading" :disabled="loading"
style=" class="w-full px-[0.9rem] py-[0.65rem] bg-[var(--wraith-bg-tertiary)] border border-[var(--wraith-border)] rounded-[6px] text-[var(--wraith-text-primary)] text-[0.95rem] outline-none transition-colors duration-150 box-border focus:border-[var(--wraith-accent-blue)]"
width: 100%;
padding: 0.65rem 0.9rem;
background-color: var(--wraith-bg-tertiary);
border: 1px solid var(--wraith-border);
border-radius: 6px;
color: var(--wraith-text-primary);
font-size: 0.95rem;
outline: none;
transition: border-color 0.15s ease;
box-sizing: border-box;
"
@focus="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-accent-blue)'"
@blur="($event.target as HTMLInputElement).style.borderColor = 'var(--wraith-border)'"
/> />
<p <p class="mt-[0.4rem] text-[0.75rem] text-[var(--wraith-text-muted)]">
style="
margin: 0.4rem 0 0;
font-size: 0.75rem;
color: var(--wraith-text-muted);
"
>
Minimum 12 characters. This password cannot be recovered. Minimum 12 characters. This password cannot be recovered.
</p> </p>
</div> </div>
@ -189,14 +108,7 @@ const displayError = computed(() => localError.value ?? app.error);
<!-- Error message --> <!-- Error message -->
<div <div
v-if="displayError" v-if="displayError"
style=" class="px-[0.9rem] py-[0.6rem] bg-[rgba(248,81,73,0.1)] border border-[rgba(248,81,73,0.3)] rounded-[6px] text-[var(--wraith-accent-red)] text-[0.85rem]"
padding: 0.6rem 0.9rem;
background-color: rgba(248, 81, 73, 0.1);
border: 1px solid rgba(248, 81, 73, 0.3);
border-radius: 6px;
color: var(--wraith-accent-red);
font-size: 0.85rem;
"
> >
{{ displayError }} {{ displayError }}
</div> </div>
@ -205,22 +117,8 @@ const displayError = computed(() => localError.value ?? app.error);
<button <button
type="submit" type="submit"
:disabled="loading" :disabled="loading"
style=" class="w-full py-[0.7rem] mt-2 bg-[var(--wraith-accent-blue)] text-[#0d1117] font-bold text-[0.9rem] tracking-[0.08em] uppercase border-none rounded-[6px] transition-[opacity,background-color] duration-150"
width: 100%; :class="loading ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'"
padding: 0.7rem;
margin-top: 0.5rem;
background-color: var(--wraith-accent-blue);
color: #0d1117;
font-weight: 700;
font-size: 0.9rem;
letter-spacing: 0.08em;
text-transform: uppercase;
border: none;
border-radius: 6px;
cursor: pointer;
transition: opacity 0.15s ease, background-color 0.15s ease;
"
:style="{ opacity: loading ? '0.6' : '1', cursor: loading ? 'not-allowed' : 'pointer' }"
> >
<span v-if="loading"> <span v-if="loading">
{{ isFirstRun ? "Creating vault..." : "Unlocking..." }} {{ isFirstRun ? "Creating vault..." : "Unlocking..." }}
@ -232,14 +130,7 @@ const displayError = computed(() => localError.value ?? app.error);
</form> </form>
<!-- Footer hint --> <!-- Footer hint -->
<p <p class="mt-6 text-center text-[0.75rem] text-[var(--wraith-text-muted)]">
style="
margin: 1.5rem 0 0;
text-align: center;
font-size: 0.75rem;
color: var(--wraith-text-muted);
"
>
<template v-if="isFirstRun"> <template v-if="isFirstRun">
Your vault will be encrypted with AES-256-GCM. Your vault will be encrypted with AES-256-GCM.
</template> </template>