wraith/frontend/components/rdp/RdpToolbar.vue
Vantz Stockwell e2e03be2dd feat: RDP frontend — Guacamole client with custom JSON WebSocket tunnel
- useRdp.ts: JsonWsTunnel class extends Guacamole.Tunnel to bridge
  guacamole-common-js (expects raw protocol) with our JSON gateway
  (consistent with SSH/SFTP message envelope pattern). Parses
  length-prefixed Guacamole instructions, dispatches to Guacamole.Client.
  Handles mouse/keyboard input, clipboard send, and session lifecycle.
- RdpCanvas.vue: full-size container that mounts the Guacamole display
  canvas. Calls useRdp().connectRdp() on mount, cleans up on unmount.
  Exposes sendClipboard() and disconnect() for toolbar integration.
- RdpToolbar.vue: auto-hiding floating toolbar (3s idle timeout) with
  clipboard paste dialog, fullscreen toggle (HTML5 Fullscreen API),
  settings panel stub, and disconnect button.
- SessionContainer.vue: renders RdpCanvas + RdpToolbar when
  session.protocol === 'rdp', replacing the Phase 3 placeholder.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:27:19 -04:00

227 lines
6.3 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue'
import { Monitor, Clipboard, Maximize2, Minimize2, X, Settings } from 'lucide-vue-next'
const props = defineProps<{
hostName: string
}>()
const emit = defineEmits<{
disconnect: []
sendClipboard: [text: string]
}>()
// Toolbar visibility — auto-hide after idle
const toolbarVisible = ref(true)
let hideTimer: ReturnType<typeof setTimeout> | null = null
function showToolbar() {
toolbarVisible.value = true
if (hideTimer) clearTimeout(hideTimer)
hideTimer = setTimeout(() => {
toolbarVisible.value = false
}, 3000)
}
// Clipboard dialog
const clipboardOpen = ref(false)
const clipboardText = ref('')
function openClipboard() {
clipboardOpen.value = true
clipboardText.value = ''
}
function pasteClipboard() {
if (clipboardText.value) {
emit('sendClipboard', clipboardText.value)
}
clipboardOpen.value = false
}
// Fullscreen
const isFullscreen = ref(false)
async function toggleFullscreen() {
if (!document.fullscreenElement) {
await document.documentElement.requestFullscreen()
isFullscreen.value = true
} else {
await document.exitFullscreen()
isFullscreen.value = false
}
}
// Settings panel
const settingsOpen = ref(false)
// Disconnect
function disconnect() {
emit('disconnect')
}
</script>
<template>
<!-- Floating toolbar shows on mouse movement, auto-hides after 3s -->
<div
class="rdp-toolbar-wrapper"
@mousemove="showToolbar"
>
<Transition name="toolbar-slide">
<div
v-if="toolbarVisible"
class="rdp-toolbar"
>
<!-- Host name label -->
<div class="flex items-center gap-2 text-gray-300 text-sm font-medium min-w-0">
<Monitor class="w-4 h-4 text-wraith-400 shrink-0" />
<span class="truncate max-w-36">{{ props.hostName }}</span>
</div>
<div class="h-4 w-px bg-gray-600 mx-1" />
<!-- Clipboard -->
<button
class="toolbar-btn"
title="Send clipboard text"
@click="openClipboard"
>
<Clipboard class="w-4 h-4" />
</button>
<!-- Fullscreen toggle -->
<button
class="toolbar-btn"
:title="isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'"
@click="toggleFullscreen"
>
<Maximize2 v-if="!isFullscreen" class="w-4 h-4" />
<Minimize2 v-else class="w-4 h-4" />
</button>
<!-- Settings -->
<button
class="toolbar-btn"
title="RDP settings"
@click="settingsOpen = !settingsOpen"
>
<Settings class="w-4 h-4" />
</button>
<div class="h-4 w-px bg-gray-600 mx-1" />
<!-- Disconnect -->
<button
class="toolbar-btn toolbar-btn-danger"
title="Disconnect"
@click="disconnect"
>
<X class="w-4 h-4" />
</button>
</div>
</Transition>
<!-- Clipboard Dialog -->
<Teleport to="body">
<div
v-if="clipboardOpen"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
@click.self="clipboardOpen = false"
>
<div class="bg-gray-800 border border-gray-700 rounded-xl shadow-2xl w-full max-w-md p-6">
<h2 class="text-gray-100 font-semibold text-lg mb-4 flex items-center gap-2">
<Clipboard class="w-5 h-5 text-wraith-400" />
Send to clipboard
</h2>
<p class="text-gray-400 text-sm mb-3">
Type or paste text here. It will be sent to the remote session clipboard.
</p>
<textarea
v-model="clipboardText"
class="w-full bg-gray-900 border border-gray-600 rounded-lg text-gray-100 text-sm p-3 resize-none focus:outline-none focus:ring-2 focus:ring-wraith-500"
rows="5"
placeholder="Paste text to send..."
autofocus
/>
<div class="flex justify-end gap-3 mt-4">
<button
class="px-4 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-gray-300 text-sm transition-colors"
@click="clipboardOpen = false"
>
Cancel
</button>
<button
class="px-4 py-2 rounded-lg bg-wraith-600 hover:bg-wraith-500 text-white text-sm font-medium transition-colors"
:disabled="!clipboardText"
@click="pasteClipboard"
>
Send
</button>
</div>
</div>
</div>
</Teleport>
<!-- Settings panel (minimal — expand in later tasks) -->
<Teleport to="body">
<div
v-if="settingsOpen"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
@click.self="settingsOpen = false"
>
<div class="bg-gray-800 border border-gray-700 rounded-xl shadow-2xl w-full max-w-sm p-6">
<h2 class="text-gray-100 font-semibold text-lg mb-4 flex items-center gap-2">
<Settings class="w-5 h-5 text-wraith-400" />
RDP Settings
</h2>
<p class="text-gray-400 text-sm">
Advanced RDP settings (color depth, resize behavior) will be configurable here in a future release.
</p>
<div class="flex justify-end mt-6">
<button
class="px-4 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-gray-300 text-sm transition-colors"
@click="settingsOpen = false"
>
Close
</button>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<style scoped>
.rdp-toolbar-wrapper {
@apply absolute inset-0 pointer-events-none z-20;
}
.rdp-toolbar {
@apply absolute top-4 left-1/2 -translate-x-1/2
flex items-center gap-1 px-3 py-1.5
bg-gray-900/90 backdrop-blur-sm
border border-gray-700 rounded-full shadow-xl
pointer-events-auto;
}
.toolbar-btn {
@apply p-1.5 rounded-full text-gray-400 hover:text-gray-100 hover:bg-gray-700
transition-colors duration-150;
}
.toolbar-btn-danger {
@apply hover:text-red-400 hover:bg-red-900/30;
}
/* Toolbar slide-down animation */
.toolbar-slide-enter-active,
.toolbar-slide-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.toolbar-slide-enter-from,
.toolbar-slide-leave-to {
opacity: 0;
transform: translateX(-50%) translateY(-8px);
}
</style>