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>
<div class="flex flex-col h-full p-4 gap-3">
<div class="flex items-center gap-2">
<ToolShell ref="shell" placeholder="Select a mode and click Run Test">
<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">
<option value="speedtest">Internet Speed Test</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">
{{ running ? "Testing..." : "Run Test" }}
</button>
</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 || "Select a mode and click Run Test" }}</pre>
</div>
</template>
</ToolShell>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import ToolShell from "./ToolShell.vue";
const props = defineProps<{ sessionId: string }>();
const mode = ref("speedtest");
const server = ref("");
const duration = ref(5);
const output = ref("");
const running = ref(false);
const shell = ref<InstanceType<typeof ToolShell> | null>(null);
async function run(): Promise<void> {
running.value = true;
output.value = mode.value === "iperf" ? `Running iperf3 to ${server.value}...\n` : "Running speed test...\n";
try {
if (mode.value === "iperf" && !server.value) {
shell.value?.setOutput("Enter an iperf3 server IP");
return;
}
shell.value?.execute(() => {
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 });
return invoke<string>("tool_bandwidth_iperf", { sessionId: props.sessionId, server: server.value, duration: duration.value });
}
} catch (err) { output.value = String(err); }
running.value = false;
return invoke<string>("tool_bandwidth_speedtest", { sessionId: props.sessionId });
});
}
</script>

View File

@ -1,31 +1,29 @@
<template>
<div class="flex flex-col h-full p-4 gap-3">
<div class="flex items-center gap-2">
<ToolShell ref="shell" placeholder="Enter a domain and click Lookup">
<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" />
<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>
</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>
</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 || "Enter a domain and click Lookup" }}</pre>
</div>
</template>
</ToolShell>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import ToolShell from "./ToolShell.vue";
const props = defineProps<{ sessionId: string }>();
const domain = ref("");
const recordType = ref("A");
const output = ref("");
const running = ref(false);
const shell = ref<InstanceType<typeof ToolShell> | null>(null);
async function lookup(): Promise<void> {
if (!domain.value) return;
running.value = true;
try {
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;
shell.value?.execute(() =>
invoke<string>("tool_dns_lookup", { sessionId: props.sessionId, domain: domain.value, recordType: recordType.value })
);
}
</script>

View File

@ -1,32 +1,28 @@
<template>
<div class="flex flex-col h-full p-4 gap-3">
<div class="flex items-center gap-2">
<ToolShell ref="shell" placeholder="Enter a host and click Ping">
<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.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>
</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 || "Enter a host and click Ping" }}</pre>
</div>
</template>
</ToolShell>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import ToolShell from "./ToolShell.vue";
const props = defineProps<{ sessionId: string }>();
const target = ref("");
const count = ref(4);
const output = ref("");
const running = ref(false);
const shell = ref<InstanceType<typeof ToolShell> | null>(null);
async function ping(): Promise<void> {
if (!target.value) return;
running.value = true;
output.value = `Pinging ${target.value}...\n`;
try {
shell.value?.execute(async () => {
const result = await invoke<{ target: string; output: string }>("tool_ping", { sessionId: props.sessionId, target: target.value, count: count.value });
output.value = result.output;
} catch (err) { output.value = String(err); }
running.value = false;
return result.output;
});
}
</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>
<div class="flex flex-col h-full p-4 gap-3">
<div class="flex items-center gap-2">
<ToolShell ref="shell" placeholder="Enter a host and click Trace">
<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" />
<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>
<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>
</div>
</template>
</ToolShell>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import ToolShell from "./ToolShell.vue";
const props = defineProps<{ sessionId: string }>();
const target = ref("");
const output = ref("");
const running = ref(false);
const shell = ref<InstanceType<typeof ToolShell> | null>(null);
async function trace(): Promise<void> {
if (!target.value) return;
running.value = true;
output.value = `Tracing route to ${target.value}...\n`;
try {
output.value = await invoke<string>("tool_traceroute", { sessionId: props.sessionId, target: target.value });
} catch (err) { output.value = String(err); }
running.value = false;
shell.value?.execute(() =>
invoke<string>("tool_traceroute", { sessionId: props.sessionId, target: target.value })
);
}
</script>

View File

@ -1,26 +1,25 @@
<template>
<div class="flex flex-col h-full p-4 gap-3">
<div class="flex items-center gap-2">
<ToolShell ref="shell" placeholder="Enter a domain or IP and click Whois">
<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" />
<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>
<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>
</div>
</template>
</ToolShell>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
import ToolShell from "./ToolShell.vue";
const props = defineProps<{ sessionId: string }>();
const target = ref("");
const output = ref("");
const running = ref(false);
const shell = ref<InstanceType<typeof ToolShell> | null>(null);
async function lookup(): Promise<void> {
if (!target.value) return;
running.value = true;
try { output.value = await invoke<string>("tool_whois", { sessionId: props.sessionId, target: target.value }); }
catch (err) { output.value = String(err); }
running.value = false;
shell.value?.execute(() =>
invoke<string>("tool_whois", { sessionId: props.sessionId, target: target.value })
);
}
</script>

View File

@ -50,68 +50,25 @@ const displayError = computed(() => localError.value ?? app.error);
</script>
<template>
<div
class="unlock-root"
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);
"
>
<div class="h-full flex items-center justify-center bg-[var(--wraith-bg-primary)]">
<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)]">
<!-- Logo -->
<div style="text-align: center; margin-bottom: 2rem">
<span
style="
font-size: 2rem;
font-weight: 800;
letter-spacing: 0.3em;
color: var(--wraith-accent-blue);
text-transform: uppercase;
font-family: 'Inter', monospace;
"
>
<div class="text-center mb-8">
<span class="text-[2rem] font-extrabold tracking-[0.3em] text-[var(--wraith-accent-blue)] uppercase font-['Inter',monospace]">
WRAITH
</span>
<p
style="
margin: 0.5rem 0 0;
font-size: 0.8rem;
color: var(--wraith-text-muted);
letter-spacing: 0.15em;
text-transform: uppercase;
"
>
<p class="mt-2 text-[0.8rem] text-[var(--wraith-text-muted)] tracking-[0.15em] uppercase">
{{ isFirstRun ? "Initialize Secure Vault" : "Secure Desktop" }}
</p>
</div>
<!-- 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 -->
<div>
<label
for="master-password"
style="
display: block;
margin-bottom: 0.4rem;
font-size: 0.8rem;
color: var(--wraith-text-secondary);
letter-spacing: 0.05em;
"
class="block mb-[0.4rem] text-[0.8rem] text-[var(--wraith-text-secondary)] tracking-[0.05em]"
>
MASTER PASSWORD
</label>
@ -122,20 +79,7 @@ const displayError = computed(() => localError.value ?? app.error);
autocomplete="current-password"
placeholder="Enter master password"
:disabled="loading"
style="
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)'"
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)]"
/>
</div>
@ -143,13 +87,7 @@ const displayError = computed(() => localError.value ?? app.error);
<div v-if="isFirstRun">
<label
for="confirm-password"
style="
display: block;
margin-bottom: 0.4rem;
font-size: 0.8rem;
color: var(--wraith-text-secondary);
letter-spacing: 0.05em;
"
class="block mb-[0.4rem] text-[0.8rem] text-[var(--wraith-text-secondary)] tracking-[0.05em]"
>
CONFIRM PASSWORD
</label>
@ -160,28 +98,9 @@ const displayError = computed(() => localError.value ?? app.error);
autocomplete="new-password"
placeholder="Confirm master password"
:disabled="loading"
style="
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)'"
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)]"
/>
<p
style="
margin: 0.4rem 0 0;
font-size: 0.75rem;
color: var(--wraith-text-muted);
"
>
<p class="mt-[0.4rem] text-[0.75rem] text-[var(--wraith-text-muted)]">
Minimum 12 characters. This password cannot be recovered.
</p>
</div>
@ -189,14 +108,7 @@ const displayError = computed(() => localError.value ?? app.error);
<!-- Error message -->
<div
v-if="displayError"
style="
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;
"
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]"
>
{{ displayError }}
</div>
@ -205,22 +117,8 @@ const displayError = computed(() => localError.value ?? app.error);
<button
type="submit"
:disabled="loading"
style="
width: 100%;
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' }"
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"
:class="loading ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'"
>
<span v-if="loading">
{{ isFirstRun ? "Creating vault..." : "Unlocking..." }}
@ -232,14 +130,7 @@ const displayError = computed(() => localError.value ?? app.error);
</form>
<!-- Footer hint -->
<p
style="
margin: 1.5rem 0 0;
text-align: center;
font-size: 0.75rem;
color: var(--wraith-text-muted);
"
>
<p class="mt-6 text-center text-[0.75rem] text-[var(--wraith-text-muted)]">
<template v-if="isFirstRun">
Your vault will be encrypted with AES-256-GCM.
</template>