fix: wire settings modal, import dialog, and connection edit into the UI

- Settings button (gear icon) now opens SettingsModal with General, Terminal, Vault, About sections
- File menu added to toolbar with New Connection, Import MobaXTerm, Settings, Exit
- Command Palette "Settings" action now opens the settings modal
- Command Palette "New SSH/RDP Connection" actions now open ConnectionEditDialog
- ConnectionEditDialog mounted in MainLayout for File menu / palette access
- All Wails binding calls left as TODO with mock behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-17 10:03:17 -04:00
parent 5cdb96ffb8
commit 473a25cf2a
3 changed files with 418 additions and 7 deletions

View File

@ -117,6 +117,8 @@ const sessionStore = useSessionStore();
const emit = defineEmits<{ const emit = defineEmits<{
(e: "open-import"): void; (e: "open-import"): void;
(e: "open-settings"): void;
(e: "open-new-connection", protocol?: "ssh" | "rdp"): void;
}>(); }>();
const actions: PaletteAction[] = [ const actions: PaletteAction[] = [
@ -125,7 +127,7 @@ const actions: PaletteAction[] = [
label: "New SSH Connection", label: "New SSH Connection",
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4"><path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM7.25 8a.749.749 0 0 1-.22.53l-2.25 2.25a.749.749 0 1 1-1.06-1.06L5.44 8 3.72 6.28a.749.749 0 1 1 1.06-1.06l2.25 2.25c.141.14.22.331.22.53Zm1.5 1.5h3a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5Z"/></svg>`, icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4"><path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM7.25 8a.749.749 0 0 1-.22.53l-2.25 2.25a.749.749 0 1 1-1.06-1.06L5.44 8 3.72 6.28a.749.749 0 1 1 1.06-1.06l2.25 2.25c.141.14.22.331.22.53Zm1.5 1.5h3a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5Z"/></svg>`,
handler: () => { handler: () => {
// TODO: Open new connection dialog for SSH emit("open-new-connection", "ssh");
close(); close();
}, },
}, },
@ -134,7 +136,7 @@ const actions: PaletteAction[] = [
label: "New RDP Connection", label: "New RDP Connection",
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4"><path d="M1.75 2.5h12.5a.25.25 0 0 1 .25.25v7.5a.25.25 0 0 1-.25.25H1.75a.25.25 0 0 1-.25-.25v-7.5a.25.25 0 0 1 .25-.25ZM14.25 1H1.75A1.75 1.75 0 0 0 0 2.75v7.5C0 11.216.784 12 1.75 12h4.388l-.533 1.5H4a.75.75 0 0 0 0 1.5h8a.75.75 0 0 0 0-1.5h-1.605l-.533-1.5h4.388A1.75 1.75 0 0 0 16 10.25v-7.5A1.75 1.75 0 0 0 14.25 1ZM9.112 13.5H6.888l.533-1.5h1.158l.533 1.5Z"/></svg>`, icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4"><path d="M1.75 2.5h12.5a.25.25 0 0 1 .25.25v7.5a.25.25 0 0 1-.25.25H1.75a.25.25 0 0 1-.25-.25v-7.5a.25.25 0 0 1 .25-.25ZM14.25 1H1.75A1.75 1.75 0 0 0 0 2.75v7.5C0 11.216.784 12 1.75 12h4.388l-.533 1.5H4a.75.75 0 0 0 0 1.5h8a.75.75 0 0 0 0-1.5h-1.605l-.533-1.5h4.388A1.75 1.75 0 0 0 16 10.25v-7.5A1.75 1.75 0 0 0 14.25 1ZM9.112 13.5H6.888l.533-1.5h1.158l.533 1.5Z"/></svg>`,
handler: () => { handler: () => {
// TODO: Open new connection dialog for RDP emit("open-new-connection", "rdp");
close(); close();
}, },
}, },
@ -152,7 +154,7 @@ const actions: PaletteAction[] = [
label: "Settings", label: "Settings",
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4"><path d="M8 0a8.2 8.2 0 0 1 .701.031C8.955.017 9.209 0 9.466 0a1.934 1.934 0 0 1 1.466.665c.33.367.51.831.54 1.316a7.96 7.96 0 0 1 .82.4c.463-.207.97-.29 1.476-.19.504.1.963.37 1.3.77.339.404.516.91.5 1.423a1.94 1.94 0 0 1-.405 1.168 8.02 8.02 0 0 1 .356.9 1.939 1.939 0 0 1 1.48.803 1.941 1.941 0 0 1 0 2.29 1.939 1.939 0 0 1-1.48.803c-.095.316-.215.622-.357.9a1.94 1.94 0 0 1-.094 2.59 1.94 1.94 0 0 1-2.776.22 7.96 7.96 0 0 1-.82.4 1.94 1.94 0 0 1-2.006 1.98A8.2 8.2 0 0 1 8 16a8.2 8.2 0 0 1-.701-.031 1.938 1.938 0 0 1-2.005-1.98 7.96 7.96 0 0 1-.82-.4 1.94 1.94 0 0 1-2.776-.22 1.94 1.94 0 0 1-.094-2.59 8.02 8.02 0 0 1-.357-.9A1.939 1.939 0 0 1 0 8.945a1.941 1.941 0 0 1 0-2.29 1.939 1.939 0 0 1 1.247-.803c.095-.316.215-.622.357-.9a1.94 1.94 0 0 1 .094-2.59 1.94 1.94 0 0 1 2.776-.22c.258-.157.532-.293.82-.4A1.934 1.934 0 0 1 6.834.665 1.934 1.934 0 0 1 8.3.03 8.2 8.2 0 0 1 8 0ZM8 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg>`, icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4"><path d="M8 0a8.2 8.2 0 0 1 .701.031C8.955.017 9.209 0 9.466 0a1.934 1.934 0 0 1 1.466.665c.33.367.51.831.54 1.316a7.96 7.96 0 0 1 .82.4c.463-.207.97-.29 1.476-.19.504.1.963.37 1.3.77.339.404.516.91.5 1.423a1.94 1.94 0 0 1-.405 1.168 8.02 8.02 0 0 1 .356.9 1.939 1.939 0 0 1 1.48.803 1.941 1.941 0 0 1 0 2.29 1.939 1.939 0 0 1-1.48.803c-.095.316-.215.622-.357.9a1.94 1.94 0 0 1-.094 2.59 1.94 1.94 0 0 1-2.776.22 7.96 7.96 0 0 1-.82.4 1.94 1.94 0 0 1-2.006 1.98A8.2 8.2 0 0 1 8 16a8.2 8.2 0 0 1-.701-.031 1.938 1.938 0 0 1-2.005-1.98 7.96 7.96 0 0 1-.82-.4 1.94 1.94 0 0 1-2.776-.22 1.94 1.94 0 0 1-.094-2.59 8.02 8.02 0 0 1-.357-.9A1.939 1.939 0 0 1 0 8.945a1.941 1.941 0 0 1 0-2.29 1.939 1.939 0 0 1 1.247-.803c.095-.316.215-.622.357-.9a1.94 1.94 0 0 1 .094-2.59 1.94 1.94 0 0 1 2.776-.22c.258-.157.532-.293.82-.4A1.934 1.934 0 0 1 6.834.665 1.934 1.934 0 0 1 8.3.03 8.2 8.2 0 0 1 8 0ZM8 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg>`,
handler: () => { handler: () => {
// TODO: Open settings dialog emit("open-settings");
close(); close();
}, },
}, },

View File

@ -0,0 +1,314 @@
<template>
<Teleport to="body">
<div
v-if="visible"
class="fixed inset-0 z-50 flex items-center justify-center"
@click.self="close"
@keydown.esc="close"
>
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/50" @click="close" />
<!-- Modal -->
<div class="relative w-full max-w-lg bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden">
<!-- Header -->
<div class="flex items-center justify-between px-4 py-3 border-b border-[#30363d]">
<h3 class="text-sm font-semibold text-[var(--wraith-text-primary)]">Settings</h3>
<button
class="text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
@click="close"
>
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.749.749 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.749.749 0 1 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z" />
</svg>
</button>
</div>
<!-- Body -->
<div class="flex min-h-[400px] max-h-[70vh]">
<!-- Section tabs (left sidebar) -->
<div class="w-36 border-r border-[#30363d] py-2 shrink-0">
<button
v-for="section in sections"
:key="section.id"
class="w-full flex items-center gap-2 px-4 py-2 text-xs text-left transition-colors cursor-pointer"
:class="activeSection === section.id
? 'bg-[#1f6feb]/20 text-[var(--wraith-text-primary)] border-r-2 border-[#1f6feb]'
: 'text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)]'
"
@click="activeSection = section.id"
>
<span v-html="section.icon" />
{{ section.label }}
</button>
</div>
<!-- Section content (right panel) -->
<div class="flex-1 overflow-y-auto p-4 space-y-4">
<!-- General -->
<template v-if="activeSection === 'general'">
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">General</h4>
<!-- Default protocol -->
<div>
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Default Protocol</label>
<select
v-model="settings.defaultProtocol"
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors cursor-pointer"
>
<option value="ssh">SSH</option>
<option value="rdp">RDP</option>
</select>
<!-- TODO: Persist via Wails binding SettingsService.SetDefaultProtocol(value) -->
</div>
<!-- Sidebar width -->
<div>
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">
Sidebar Width: {{ settings.sidebarWidth }}px
</label>
<input
v-model.number="settings.sidebarWidth"
type="range"
min="180"
max="400"
step="10"
class="w-full accent-[var(--wraith-accent-blue)]"
/>
<!-- TODO: Persist via Wails binding SettingsService.SetSidebarWidth(value) -->
</div>
</template>
<!-- Terminal -->
<template v-if="activeSection === 'terminal'">
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">Terminal</h4>
<!-- Default theme -->
<div>
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Default Theme</label>
<select
v-model="settings.terminalTheme"
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors cursor-pointer"
>
<option v-for="theme in themeNames" :key="theme" :value="theme">{{ theme }}</option>
</select>
<!-- TODO: Persist via Wails binding SettingsService.SetTerminalTheme(value) -->
</div>
<!-- Font size -->
<div>
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Font Size</label>
<input
v-model.number="settings.fontSize"
type="number"
min="8"
max="32"
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
/>
<!-- TODO: Persist via Wails binding SettingsService.SetFontSize(value) -->
</div>
<!-- Scrollback buffer -->
<div>
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-1">Scrollback Buffer (lines)</label>
<input
v-model.number="settings.scrollbackBuffer"
type="number"
min="100"
max="100000"
step="100"
class="w-full px-3 py-2 text-sm rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] transition-colors"
/>
<!-- TODO: Persist via Wails binding SettingsService.SetScrollbackBuffer(value) -->
</div>
</template>
<!-- Vault -->
<template v-if="activeSection === 'vault'">
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">Vault</h4>
<!-- Change master password -->
<div>
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-2">Master Password</label>
<button
class="px-3 py-2 text-xs text-[var(--wraith-text-primary)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
@click="changeMasterPassword"
>
Change Master Password
</button>
<!-- TODO: Open a password change dialog via Wails binding VaultService.ChangeMasterPassword(old, new) -->
</div>
<!-- Export vault -->
<div class="pt-2">
<label class="block text-xs text-[var(--wraith-text-secondary)] mb-2">Backup</label>
<div class="flex gap-2">
<button
class="px-3 py-2 text-xs text-[var(--wraith-text-primary)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
@click="exportVault"
>
Export Vault
</button>
<button
class="px-3 py-2 text-xs text-[var(--wraith-text-primary)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
@click="importVault"
>
Import Vault
</button>
</div>
<!-- TODO: Wails bindings VaultService.Export() / VaultService.Import(data) -->
</div>
</template>
<!-- About -->
<template v-if="activeSection === 'about'">
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">About</h4>
<div class="space-y-3">
<div class="bg-[#0d1117] rounded-lg p-4 text-center">
<div class="text-lg font-bold tracking-widest text-[var(--wraith-accent-blue)] mb-1">WRAITH</div>
<div class="text-xs text-[var(--wraith-text-secondary)]">Connection Manager</div>
</div>
<div class="space-y-2 text-xs">
<div class="flex justify-between py-1.5 border-b border-[#30363d]">
<span class="text-[var(--wraith-text-secondary)]">Version</span>
<span class="text-[var(--wraith-text-primary)]">0.1.0-dev</span>
<!-- TODO: Fetch from Wails binding AppService.GetVersion() -->
</div>
<div class="flex justify-between py-1.5 border-b border-[#30363d]">
<span class="text-[var(--wraith-text-secondary)]">License</span>
<span class="text-[var(--wraith-text-primary)]">Proprietary</span>
</div>
<div class="flex justify-between py-1.5 border-b border-[#30363d]">
<span class="text-[var(--wraith-text-secondary)]">Runtime</span>
<span class="text-[var(--wraith-text-primary)]">Wails v2</span>
</div>
<div class="flex justify-between py-1.5">
<span class="text-[var(--wraith-text-secondary)]">Frontend</span>
<span class="text-[var(--wraith-text-primary)]">Vue 3 + TypeScript</span>
</div>
</div>
<div class="flex gap-2 pt-2">
<a
href="#"
class="flex-1 px-3 py-2 text-xs text-center text-[var(--wraith-accent-blue)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
@click.prevent="openLink('docs')"
>
Documentation
</a>
<a
href="#"
class="flex-1 px-3 py-2 text-xs text-center text-[var(--wraith-accent-blue)] rounded border border-[#30363d] hover:bg-[#30363d] transition-colors cursor-pointer"
@click.prevent="openLink('repo')"
>
Source Code
</a>
</div>
<!-- TODO: Wails binding runtime.BrowserOpenURL(url) -->
</div>
</template>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-end px-4 py-3 border-t border-[#30363d]">
<button
class="px-3 py-1.5 text-xs text-white bg-[#1f6feb] hover:bg-[#388bfd] rounded transition-colors cursor-pointer"
@click="close"
>
Done
</button>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref } from "vue";
type Section = "general" | "terminal" | "vault" | "about";
const visible = ref(false);
const activeSection = ref<Section>("general");
const sections = [
{
id: "general" as const,
label: "General",
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M8 0a8.2 8.2 0 0 1 .701.031C8.955.017 9.209 0 9.466 0a1.934 1.934 0 0 1 1.466.665c.33.367.51.831.54 1.316a7.96 7.96 0 0 1 .82.4c.463-.207.97-.29 1.476-.19.504.1.963.37 1.3.77.339.404.516.91.5 1.423a1.94 1.94 0 0 1-.405 1.168 8.02 8.02 0 0 1 .356.9 1.939 1.939 0 0 1 1.48.803 1.941 1.941 0 0 1 0 2.29 1.939 1.939 0 0 1-1.48.803c-.095.316-.215.622-.357.9a1.94 1.94 0 0 1-.094 2.59 1.94 1.94 0 0 1-2.776.22 7.96 7.96 0 0 1-.82.4 1.94 1.94 0 0 1-2.006 1.98A8.2 8.2 0 0 1 8 16a8.2 8.2 0 0 1-.701-.031 1.938 1.938 0 0 1-2.005-1.98 7.96 7.96 0 0 1-.82-.4 1.94 1.94 0 0 1-2.776-.22 1.94 1.94 0 0 1-.094-2.59 8.02 8.02 0 0 1-.357-.9A1.939 1.939 0 0 1 0 8.945a1.941 1.941 0 0 1 0-2.29 1.939 1.939 0 0 1 1.247-.803c.095-.316.215-.622.357-.9a1.94 1.94 0 0 1 .094-2.59 1.94 1.94 0 0 1 2.776-.22c.258-.157.532-.293.82-.4A1.934 1.934 0 0 1 6.834.665 1.934 1.934 0 0 1 8.3.03 8.2 8.2 0 0 1 8 0ZM8 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg>`,
},
{
id: "terminal" as const,
label: "Terminal",
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM7.25 8a.749.749 0 0 1-.22.53l-2.25 2.25a.749.749 0 1 1-1.06-1.06L5.44 8 3.72 6.28a.749.749 0 1 1 1.06-1.06l2.25 2.25c.141.14.22.331.22.53Zm1.5 1.5h3a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5Z"/></svg>`,
},
{
id: "vault" as const,
label: "Vault",
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M4 4v2h-.25A1.75 1.75 0 0 0 2 7.75v5.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0 0 14 13.25v-5.5A1.75 1.75 0 0 0 12.25 6H12V4a4 4 0 1 0-8 0Zm6.5 2V4a2.5 2.5 0 0 0-5 0v2ZM8 9.5a1.5 1.5 0 0 1 .5 2.915V13.5a.5.5 0 0 1-1 0v-1.085A1.5 1.5 0 0 1 8 9.5Z"/></svg>`,
},
{
id: "about" as const,
label: "About",
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>`,
},
];
const themeNames = [
"Dracula",
"Nord",
"Monokai",
"One Dark",
"Solarized Dark",
"Gruvbox Dark",
"MobaXTerm Classic",
];
const settings = ref({
defaultProtocol: "ssh" as "ssh" | "rdp",
sidebarWidth: 240,
terminalTheme: "Dracula",
fontSize: 14,
scrollbackBuffer: 5000,
});
function open(): void {
visible.value = true;
activeSection.value = "general";
}
function close(): void {
visible.value = false;
}
function changeMasterPassword(): void {
// TODO: Replace with Wails binding open a password change dialog
// VaultService.ChangeMasterPassword(oldPassword, newPassword)
alert("Change master password — not yet implemented (requires Wails binding)");
}
function exportVault(): void {
// TODO: Replace with Wails binding VaultService.Export()
alert("Export vault — not yet implemented (requires Wails binding)");
}
function importVault(): void {
// TODO: Replace with Wails binding VaultService.Import(data)
alert("Import vault — not yet implemented (requires Wails binding)");
}
function openLink(target: string): void {
// TODO: Replace with Wails runtime.BrowserOpenURL(url)
const urls: Record<string, string> = {
docs: "https://github.com/wraith/docs",
repo: "https://github.com/wraith",
};
console.log("Open link:", urls[target] ?? target);
}
defineExpose({ open, close, visible });
</script>

View File

@ -5,10 +5,61 @@
class="h-10 flex items-center justify-between px-4 bg-[var(--wraith-bg-secondary)] border-b border-[var(--wraith-border)] shrink-0" class="h-10 flex items-center justify-between px-4 bg-[var(--wraith-bg-secondary)] border-b border-[var(--wraith-border)] shrink-0"
style="--wails-draggable: drag" style="--wails-draggable: drag"
> >
<span class="text-sm font-bold tracking-widest text-[var(--wraith-accent-blue)]"> <div class="flex items-center gap-3" style="--wails-draggable: no-drag">
<span class="text-sm font-bold tracking-widest text-[var(--wraith-accent-blue)]" style="--wails-draggable: drag">
WRAITH WRAITH
</span> </span>
<!-- File menu -->
<div class="relative">
<button
class="text-xs text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer px-2 py-1 rounded hover:bg-[var(--wraith-bg-tertiary)]"
@click="showFileMenu = !showFileMenu"
@blur="closeFileMenuDeferred"
>
File
</button>
<div
v-if="showFileMenu"
class="absolute top-full left-0 mt-0.5 w-56 bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden z-50 py-1"
>
<button
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
@mousedown.prevent="handleFileMenuAction('new-connection')"
>
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"/></svg>
<span class="flex-1">New Connection</span>
<kbd class="text-[10px] text-[var(--wraith-text-muted)]">Ctrl+N</kbd>
</button>
<div class="border-t border-[#30363d] my-1" />
<button
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
@mousedown.prevent="handleFileMenuAction('import')"
>
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="currentColor"><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14ZM11.78 4.72a.749.749 0 1 1-1.06 1.06L8.75 3.81V9.5a.75.75 0 0 1-1.5 0V3.81L5.28 5.78a.749.749 0 1 1-1.06-1.06l3.25-3.25a.749.749 0 0 1 1.06 0l3.25 3.25Z"/></svg>
<span class="flex-1">Import from MobaXTerm</span>
</button>
<div class="border-t border-[#30363d] my-1" />
<button
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
@mousedown.prevent="handleFileMenuAction('settings')"
>
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0a8.2 8.2 0 0 1 .701.031C8.955.017 9.209 0 9.466 0a1.934 1.934 0 0 1 1.466.665c.33.367.51.831.54 1.316a7.96 7.96 0 0 1 .82.4c.463-.207.97-.29 1.476-.19.504.1.963.37 1.3.77.339.404.516.91.5 1.423a1.94 1.94 0 0 1-.405 1.168 8.02 8.02 0 0 1 .356.9 1.939 1.939 0 0 1 1.48.803 1.941 1.941 0 0 1 0 2.29 1.939 1.939 0 0 1-1.48.803c-.095.316-.215.622-.357.9a1.94 1.94 0 0 1-.094 2.59 1.94 1.94 0 0 1-2.776.22 7.96 7.96 0 0 1-.82.4 1.94 1.94 0 0 1-2.006 1.98A8.2 8.2 0 0 1 8 16a8.2 8.2 0 0 1-.701-.031 1.938 1.938 0 0 1-2.005-1.98 7.96 7.96 0 0 1-.82-.4 1.94 1.94 0 0 1-2.776-.22 1.94 1.94 0 0 1-.094-2.59 8.02 8.02 0 0 1-.357-.9A1.939 1.939 0 0 1 0 8.945a1.941 1.941 0 0 1 0-2.29 1.939 1.939 0 0 1 1.247-.803c.095-.316.215-.622.357-.9a1.94 1.94 0 0 1 .094-2.59 1.94 1.94 0 0 1 2.776-.22c.258-.157.532-.293.82-.4A1.934 1.934 0 0 1 6.834.665 1.934 1.934 0 0 1 8.3.03 8.2 8.2 0 0 1 8 0ZM8 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg>
<span class="flex-1">Settings</span>
</button>
<div class="border-t border-[#30363d] my-1" />
<button
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
@mousedown.prevent="handleFileMenuAction('exit')"
>
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="currentColor"><path d="M2 2.75C2 1.784 2.784 1 3.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5a.75.75 0 0 1-1.5 0v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h3.5a.75.75 0 0 1 0 1.5h-3.5A1.75 1.75 0 0 1 2 13.25Zm10.44 4.5-1.97-1.97a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l1.97-1.97H6.75a.75.75 0 0 1 0-1.5Z"/></svg>
<span class="flex-1">Exit</span>
<kbd class="text-[10px] text-[var(--wraith-text-muted)]">Alt+F4</kbd>
</button>
</div>
</div>
</div>
<!-- Quick Connect --> <!-- Quick Connect -->
<div class="flex-1 max-w-xs mx-4" style="--wails-draggable: no-drag"> <div class="flex-1 max-w-xs mx-4" style="--wails-draggable: no-drag">
<input <input
@ -60,6 +111,7 @@
<button <button
class="hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer" class="hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
title="Settings" title="Settings"
@click="settingsModal?.open()"
> >
&#x2699; &#x2699;
</button> </button>
@ -137,13 +189,24 @@
<StatusBar ref="statusBar" @open-theme-picker="themePicker?.open()" /> <StatusBar ref="statusBar" @open-theme-picker="themePicker?.open()" />
<!-- Command Palette (Ctrl+K) --> <!-- Command Palette (Ctrl+K) -->
<CommandPalette ref="commandPalette" @open-import="importDialog?.open()" /> <CommandPalette
ref="commandPalette"
@open-import="importDialog?.open()"
@open-settings="settingsModal?.open()"
@open-new-connection="connectionEditDialog?.openNew()"
/>
<!-- Theme Picker --> <!-- Theme Picker -->
<ThemePicker ref="themePicker" @select="handleThemeSelect" /> <ThemePicker ref="themePicker" @select="handleThemeSelect" />
<!-- Import Dialog --> <!-- Import Dialog -->
<ImportDialog ref="importDialog" /> <ImportDialog ref="importDialog" />
<!-- Settings Modal -->
<SettingsModal ref="settingsModal" />
<!-- Connection Edit Dialog (for File menu / Command Palette new connection) -->
<ConnectionEditDialog ref="connectionEditDialog" />
</div> </div>
</template> </template>
@ -164,6 +227,8 @@ import EditorWindow from "@/components/editor/EditorWindow.vue";
import CommandPalette from "@/components/common/CommandPalette.vue"; import CommandPalette from "@/components/common/CommandPalette.vue";
import ThemePicker from "@/components/common/ThemePicker.vue"; import ThemePicker from "@/components/common/ThemePicker.vue";
import ImportDialog from "@/components/common/ImportDialog.vue"; import ImportDialog from "@/components/common/ImportDialog.vue";
import SettingsModal from "@/components/common/SettingsModal.vue";
import ConnectionEditDialog from "@/components/connections/ConnectionEditDialog.vue";
import CopilotPanel from "@/components/copilot/CopilotPanel.vue"; import CopilotPanel from "@/components/copilot/CopilotPanel.vue";
import type { ThemeDefinition } from "@/components/common/ThemePicker.vue"; import type { ThemeDefinition } from "@/components/common/ThemePicker.vue";
import type { SidebarTab } from "@/components/sidebar/SidebarToggle.vue"; import type { SidebarTab } from "@/components/sidebar/SidebarToggle.vue";
@ -184,8 +249,38 @@ const editorFile = ref<{ content: string; path: string; sessionId: string } | nu
const commandPalette = ref<InstanceType<typeof CommandPalette> | null>(null); const commandPalette = ref<InstanceType<typeof CommandPalette> | null>(null);
const themePicker = ref<InstanceType<typeof ThemePicker> | null>(null); const themePicker = ref<InstanceType<typeof ThemePicker> | null>(null);
const importDialog = ref<InstanceType<typeof ImportDialog> | null>(null); const importDialog = ref<InstanceType<typeof ImportDialog> | null>(null);
const settingsModal = ref<InstanceType<typeof SettingsModal> | null>(null);
const connectionEditDialog = ref<InstanceType<typeof ConnectionEditDialog> | null>(null);
const statusBar = ref<InstanceType<typeof StatusBar> | null>(null); const statusBar = ref<InstanceType<typeof StatusBar> | null>(null);
/** File menu dropdown state. */
const showFileMenu = ref(false);
/** Close the file menu after a short delay (allows click events to fire first). */
function closeFileMenuDeferred(): void {
setTimeout(() => { showFileMenu.value = false; }, 150);
}
/** Handle file menu item clicks. */
function handleFileMenuAction(action: string): void {
showFileMenu.value = false;
switch (action) {
case "new-connection":
connectionEditDialog.value?.openNew();
break;
case "import":
importDialog.value?.open();
break;
case "settings":
settingsModal.value?.open();
break;
case "exit":
// TODO: Replace with Wails runtime.Quit()
window.close();
break;
}
}
/** Handle file open from SFTP sidebar -- loads mock content for now. */ /** Handle file open from SFTP sidebar -- loads mock content for now. */
function handleOpenFile(entry: FileEntry): void { function handleOpenFile(entry: FileEntry): void {
if (!sessionStore.activeSession) return; if (!sessionStore.activeSession) return;