feat: use xterm for terminal UI and match app controls
This commit is contained in:
Generated
+17
@@ -11,6 +11,8 @@
|
|||||||
"@scure/bip39": "^2.2.0",
|
"@scure/bip39": "^2.2.0",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.3",
|
||||||
@@ -4417,6 +4419,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xterm/addon-fit": {
|
||||||
|
"version": "0.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||||
|
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/xterm": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/abbrev": {
|
"node_modules/abbrev": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:mock-parity": "node scripts/mock-rpc-parity.mjs",
|
"test:mock-parity": "node scripts/mock-rpc-parity.mjs",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev:mock": "concurrently --raw \"node mock-backend.js\" \"VITE_AIUI_URL=http://localhost:5173 vite\" \"cd ../../AIUI && perl -MPOSIX -e 'POSIX::setsid(); exec @ARGV' -- pnpm dev 2>/dev/null || echo '[AIUI] Not found at ../../AIUI \u2014 chat will show placeholder'\"",
|
"dev:mock": "concurrently --raw \"node mock-backend.js\" \"VITE_AIUI_URL=http://localhost:5173 vite\" \"cd ../../AIUI && perl -MPOSIX -e 'POSIX::setsid(); exec @ARGV' -- pnpm dev 2>/dev/null || echo '[AIUI] Not found at ../../AIUI — chat will show placeholder'\"",
|
||||||
"dev:boot": "VITE_DEV_MODE=boot concurrently --raw \"VITE_DEV_MODE=boot node mock-backend.js\" \"VITE_DEV_MODE=boot vite\"",
|
"dev:boot": "VITE_DEV_MODE=boot concurrently --raw \"VITE_DEV_MODE=boot node mock-backend.js\" \"VITE_DEV_MODE=boot vite\"",
|
||||||
"dev:real": "echo 'Start backend: cd ../core && cargo run --release' && vite",
|
"dev:real": "echo 'Start backend: cd ../core && cargo run --release' && vite",
|
||||||
"backend:mock": "node mock-backend.js",
|
"backend:mock": "node mock-backend.js",
|
||||||
@@ -28,6 +28,8 @@
|
|||||||
"@scure/bip39": "^2.2.0",
|
"@scure/bip39": "^2.2.0",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.3",
|
||||||
|
|||||||
@@ -1,31 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="cli-popup">
|
<Transition name="cli-popup">
|
||||||
<div v-if="cliStore.isOpen" class="fixed inset-0 z-[2500] pointer-events-none p-4">
|
<div v-if="cliStore.isOpen" class="fixed inset-0 z-[2500] flex items-center justify-center p-4 pointer-events-none">
|
||||||
<section ref="panelRef" class="terminal-panel relative z-10 w-full max-w-5xl overflow-hidden pointer-events-auto" :class="{ minimized }" :style="panelStyle" aria-label="Archipelago terminal">
|
<section ref="panelRef" class="glass-card terminal-panel relative z-10 w-full overflow-hidden pointer-events-auto" :class="{ minimized }" :style="panelStyle" aria-label="Archipelago terminal">
|
||||||
<header class="terminal-header" @mousedown="startDrag">
|
<header class="terminal-header" @mousedown="startDrag">
|
||||||
<div class="terminal-title"><span class="terminal-dot"></span><span>Archipelago terminal</span><span class="terminal-host">{{ activeSession?.name || 'No session' }}</span></div>
|
<div class="terminal-drag-handle" title="Drag to move">
|
||||||
|
<svg class="w-4 h-4 text-white/50" fill="currentColor" viewBox="0 0 24 24"><path d="M8 6h2v2H8V6zm0 5h2v2H8v-2zm0 5h2v2H8v-2zm5-10h2v2h-2V6zm0 5h2v2h-2v-2zm0 5h2v2h-2v-2z" /></svg>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-title"><svg class="w-5 h-5 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg><span>CLI Access</span><span class="terminal-host">{{ activeSession?.name || 'No session' }}</span></div>
|
||||||
<div class="terminal-actions">
|
<div class="terminal-actions">
|
||||||
<select v-model="selectedId" class="terminal-select" aria-label="Terminal session" @change="connectSelected">
|
<select v-model="selectedId" class="terminal-select" aria-label="Terminal session" @change="connectSelected">
|
||||||
<option value="">Select session</option>
|
<option value="">Select session</option>
|
||||||
<option v-for="session in sessions" :key="session.id" :value="session.id">{{ session.name }} · {{ session.state }}</option>
|
<option v-for="session in sessions" :key="session.id" :value="session.id">{{ session.name }} · {{ session.state }}</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="terminal-button" type="button" @click="createSession">New</button>
|
<button class="terminal-icon-button" type="button" @click="createSession">New</button>
|
||||||
<button class="terminal-button" type="button" aria-label="Refresh terminal" title="Refresh terminal" @click="refreshTerminal">Refresh</button>
|
<button class="terminal-icon-button" type="button" aria-label="Refresh" title="Refresh" @click="refreshTerminal"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg></button>
|
||||||
<button class="terminal-button" type="button" :aria-label="minimized ? 'Restore terminal' : 'Minimize terminal'" @click="minimized = !minimized">{{ minimized ? 'Restore' : 'Minimize' }}</button>
|
<button class="terminal-icon-button" type="button" :aria-label="minimized ? 'Restore terminal' : 'Minimize terminal'" @click="minimized = !minimized"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="minimized ? 'M5 12h14' : 'M6 12h12'" /></svg></button>
|
||||||
<button class="terminal-button" type="button" @click="toggleFullscreen">{{ fullscreen ? 'Windowed' : 'Fullscreen' }}</button>
|
<button class="terminal-icon-button" type="button" :aria-label="fullscreen ? 'Windowed' : 'Fullscreen'" @click="toggleFullscreen"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="fullscreen ? 'M8 3v5H3m18 0h-5V3M3 16h5v5m8 0v-5h5' : 'M8 3H3v5m13-5h5v5M3 16v5h5m8 0h5v-5'" /></svg></button>
|
||||||
<button class="terminal-close" type="button" aria-label="Close terminal" @click="closeTerminal">Close</button>
|
<button class="terminal-icon-button" type="button" aria-label="Close" @click="closeTerminal"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg></button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="!minimized" class="terminal-body">
|
<div v-if="!minimized" class="terminal-body">
|
||||||
<div v-if="error" class="terminal-empty terminal-error"><strong>Terminal unavailable</strong><span>{{ error }}</span><button class="terminal-button" type="button" @click="loadSessions">Retry</button></div>
|
<div v-if="error" class="terminal-empty terminal-error"><strong>Terminal unavailable</strong><span>{{ error }}</span><button class="terminal-button" type="button" @click="loadSessions">Retry</button></div>
|
||||||
<div v-else-if="!sessions.length" class="terminal-empty"><strong>Create your first developer session</strong><span>Sessions stay running when this window closes and can be resumed later.</span><button class="terminal-button terminal-primary" type="button" @click="createSession">Start session</button></div>
|
<div v-else-if="!sessions.length" class="terminal-empty"><strong>Create your first developer session</strong><span>Sessions stay running when this window closes and can be resumed later.</span><button class="terminal-button terminal-primary" type="button" @click="createSession">Start session</button></div>
|
||||||
<div v-else-if="!selectedId" class="terminal-empty"><strong>Choose a session</strong><span>Resume an existing workspace or start a new one.</span></div>
|
<div v-else-if="!selectedId" class="terminal-empty"><strong>Choose a session</strong><span>Resume an existing workspace or start a new one.</span></div>
|
||||||
<pre v-else ref="outputRef" class="terminal-output" tabindex="0" @click="focusInput">{{ output }}</pre>
|
<div v-else ref="terminalRef" class="terminal-output" tabindex="0" aria-label="Archipelago terminal session" @click="focusInput"></div>
|
||||||
</div>
|
</div>
|
||||||
<footer v-if="selectedId && !minimized" class="terminal-input-bar">
|
<footer v-if="selectedId && !minimized" class="terminal-input-bar">
|
||||||
<span class="terminal-prompt">›</span><input ref="inputRef" class="terminal-input" aria-label="Terminal input" autocomplete="off" autocapitalize="off" spellcheck="false" @keydown="onKeydown" @blur="keepFocus" />
|
|
||||||
<span class="terminal-status" :class="{ connected: connected }">{{ connected ? 'Connected' : 'Reconnecting…' }}</span>
|
<span class="terminal-status" :class="{ connected: connected }">{{ connected ? 'Connected' : 'Reconnecting…' }}</span>
|
||||||
|
<span class="terminal-hint">Click terminal to focus · drag the header to move · resize from the corner</span>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,6 +38,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
import { Terminal } from '@xterm/xterm'
|
||||||
|
import { FitAddon } from '@xterm/addon-fit'
|
||||||
|
import '@xterm/xterm/css/xterm.css'
|
||||||
import { useCLIStore } from '@/stores/cli'
|
import { useCLIStore } from '@/stores/cli'
|
||||||
|
|
||||||
type Session = { id: string; name: string; workspace: string; state: string; updated_at: number }
|
type Session = { id: string; name: string; workspace: string; state: string; updated_at: number }
|
||||||
@@ -46,11 +52,12 @@ const position = ref({ x: 0, y: 0 })
|
|||||||
const dragging = ref<{ x: number; y: number; px: number; py: number } | null>(null)
|
const dragging = ref<{ x: number; y: number; px: number; py: number } | null>(null)
|
||||||
const sessions = ref<Session[]>([])
|
const sessions = ref<Session[]>([])
|
||||||
const selectedId = ref('')
|
const selectedId = ref('')
|
||||||
const output = ref('')
|
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const connected = ref(false)
|
const connected = ref(false)
|
||||||
const outputRef = ref<HTMLElement | null>(null)
|
const terminalRef = ref<HTMLElement | null>(null)
|
||||||
const inputRef = ref<HTMLInputElement | null>(null)
|
let terminal: Terminal | null = null
|
||||||
|
let fitAddon: FitAddon | null = null
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
let socket: WebSocket | null = null
|
let socket: WebSocket | null = null
|
||||||
|
|
||||||
const activeSession = computed(() => sessions.value.find(session => session.id === selectedId.value))
|
const activeSession = computed(() => sessions.value.find(session => session.id === selectedId.value))
|
||||||
@@ -98,50 +105,86 @@ async function toggleFullscreen() {
|
|||||||
}
|
}
|
||||||
function disconnect() { socket?.close(); socket = null; connected.value = false }
|
function disconnect() { socket?.close(); socket = null; connected.value = false }
|
||||||
function connectSelected() {
|
function connectSelected() {
|
||||||
disconnect(); output.value = ''
|
disconnect(); terminal?.reset()
|
||||||
if (!selectedId.value) return
|
if (!selectedId.value) return
|
||||||
socket = new WebSocket(wsUrl(selectedId.value))
|
socket = new WebSocket(wsUrl(selectedId.value))
|
||||||
socket.onopen = () => { connected.value = true; focusInput() }
|
socket.onopen = () => { connected.value = true; resizeTerminal(); focusInput() }
|
||||||
socket.onclose = () => { connected.value = false }
|
socket.onclose = () => { connected.value = false }
|
||||||
socket.onerror = () => { connected.value = false }
|
socket.onerror = () => { connected.value = false }
|
||||||
socket.onmessage = event => {
|
socket.onmessage = event => {
|
||||||
try { const message = JSON.parse(event.data); if (message.type === 'output') { output.value = message.data; nextTick(() => { if (outputRef.value) outputRef.value.scrollTop = outputRef.value.scrollHeight }) } } catch { /* ignore malformed frames */ }
|
try {
|
||||||
|
const message = JSON.parse(event.data)
|
||||||
|
if (message.type === 'output') { terminal?.write(message.data); terminal?.scrollToBottom() }
|
||||||
|
} catch { /* ignore malformed frames */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function send(data: string) { if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: 'input', data })) }
|
function resizeTerminal() {
|
||||||
function onKeydown(event: KeyboardEvent) {
|
fitAddon?.fit()
|
||||||
if (event.ctrlKey && event.key.toLowerCase() === 'c') { event.preventDefault(); send('\u0003'); return }
|
if (socket?.readyState === WebSocket.OPEN && terminal) socket.send(JSON.stringify({ type: 'resize', cols: terminal.cols, rows: terminal.rows }))
|
||||||
if (event.ctrlKey && event.key.toLowerCase() === 'd') { event.preventDefault(); send('\u0004'); return }
|
|
||||||
const special: Record<string, string> = { Enter: '\n', Backspace: '\u007f', Tab: '\t', ArrowUp: '\u001b[A', ArrowDown: '\u001b[B', ArrowRight: '\u001b[C', ArrowLeft: '\u001b[D' }
|
|
||||||
const specialKey = special[event.key]
|
|
||||||
if (specialKey !== undefined) { event.preventDefault(); send(specialKey); return }
|
|
||||||
if (event.key.length === 1 && !event.metaKey && !event.altKey) { event.preventDefault(); send(event.key) }
|
|
||||||
}
|
}
|
||||||
function focusInput() { nextTick(() => inputRef.value?.focus()) }
|
|
||||||
function keepFocus() { if (cliStore.isOpen) setTimeout(focusInput, 0) }
|
function send(data: string) { if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: 'input', data })) }
|
||||||
|
function focusInput() { nextTick(() => terminal?.focus()) }
|
||||||
watch(() => cliStore.isOpen, open => { if (open) loadSessions(); else disconnect() })
|
watch(() => cliStore.isOpen, open => { if (open) loadSessions(); else disconnect() })
|
||||||
onMounted(() => { window.addEventListener('mousemove', onDrag); window.addEventListener('mouseup', stopDrag) })
|
watch(terminalRef, element => {
|
||||||
onBeforeUnmount(() => { window.removeEventListener('mousemove', onDrag); window.removeEventListener('mouseup', stopDrag) })
|
if (!element || !terminal || element.querySelector('.xterm')) return
|
||||||
onBeforeUnmount(disconnect)
|
terminal.open(element)
|
||||||
|
fitAddon?.fit()
|
||||||
|
resizeObserver?.observe(element)
|
||||||
|
})
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('mousemove', onDrag)
|
||||||
|
window.addEventListener('mouseup', stopDrag)
|
||||||
|
terminal = new Terminal({
|
||||||
|
convertEol: true,
|
||||||
|
cursorBlink: true,
|
||||||
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: 1.45,
|
||||||
|
scrollback: 10000,
|
||||||
|
theme: { background: 'transparent', foreground: '#f3f4f6', cursor: '#f7931a', selectionBackground: 'rgba(247,147,26,.3)' },
|
||||||
|
})
|
||||||
|
fitAddon = new FitAddon()
|
||||||
|
terminal.loadAddon(fitAddon)
|
||||||
|
terminal.onData(data => send(data))
|
||||||
|
resizeObserver = new ResizeObserver(() => resizeTerminal())
|
||||||
|
if (terminalRef.value) {
|
||||||
|
terminal.open(terminalRef.value)
|
||||||
|
resizeObserver.observe(terminalRef.value)
|
||||||
|
}
|
||||||
|
nextTick(resizeTerminal)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('mousemove', onDrag)
|
||||||
|
window.removeEventListener('mouseup', stopDrag)
|
||||||
|
resizeObserver?.disconnect()
|
||||||
|
terminal?.dispose()
|
||||||
|
disconnect()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.terminal-panel { display: flex; flex-direction: column; width: min(960px, calc(100vw - 2rem)); height: min(680px, calc(100vh - 2rem)); min-width: 320px; min-height: 220px; max-width: calc(100vw - 2rem); max-height: calc(100vh - 2rem); resize: both; border: 1px solid rgba(247,147,26,.28); border-radius: 18px; background: #0b0e12; box-shadow: 0 28px 90px rgba(0,0,0,.65); color: #f6f7f9; }
|
.terminal-panel { display: flex; flex-direction: column; width: min(672px, calc(100vw - 2rem)); height: min(640px, calc(100vh - 2rem)); min-width: 320px; min-height: 220px; max-width: calc(100vw - 2rem); max-height: calc(100vh - 2rem); resize: both; }
|
||||||
.terminal-panel.minimized { width: auto; height: auto; min-width: 320px; resize: none; }
|
.terminal-panel.minimized { width: auto; height: auto; min-width: 320px; resize: none; }
|
||||||
.terminal-panel:fullscreen { width: 100vw; height: 100vh; max-width: none; max-height: none; border-radius: 0; }
|
.terminal-panel:fullscreen { width: 100vw; height: 100vh; max-width: none; max-height: none; border-radius: 0; }
|
||||||
.terminal-header, .terminal-input-bar { display: flex; align-items: center; gap: 12px; padding: 13px 16px; background: rgba(255,255,255,.045); }
|
.terminal-header, .terminal-input-bar { display: flex; align-items: center; gap: 12px; padding: 13px 16px; }
|
||||||
.terminal-header { justify-content: space-between; border-bottom: 1px solid rgba(255,255,255,.1); cursor: grab; user-select: none; }
|
.terminal-header { border-bottom: 1px solid rgba(255,255,255,.1); cursor: grab; user-select: none; }
|
||||||
.terminal-header:active { cursor: grabbing; }
|
.terminal-header:active { cursor: grabbing; }
|
||||||
.terminal-title, .terminal-actions { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
.terminal-title, .terminal-actions { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
||||||
.terminal-title { font-weight: 650; } .terminal-host { color: rgba(255,255,255,.45); font-size: 12px; font-weight: 400; }
|
.terminal-title { flex: 1; font-weight: 500; } .terminal-host { color: rgba(255,255,255,.45); font-size: 12px; font-weight: 400; }
|
||||||
.terminal-dot { width: 9px; height: 9px; border-radius: 50%; background: #f7931a; box-shadow: 0 0 12px #f7931a; }
|
.terminal-drag-handle { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; flex-shrink: 0; }
|
||||||
.terminal-body { flex: 1 1 auto; min-height: 0; height: 0; overflow: hidden; background: #050608; }
|
.terminal-drag-handle:hover { background: rgba(255,255,255,.1); }
|
||||||
.terminal-output { box-sizing: border-box; height: 100%; max-height: 100%; margin: 0; overflow-y: auto; overflow-x: hidden; padding: 18px; color: #d7f7d3; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; overflow-anchor: auto; outline: none; }
|
.terminal-body { flex: 1 1 auto; min-height: 0; height: 0; overflow: hidden; }
|
||||||
|
.terminal-output { box-sizing: border-box; height: 100%; max-height: 100%; margin: 0; overflow: hidden; padding: 14px 16px; color: rgba(255,255,255,.9); outline: none; }
|
||||||
|
.terminal-output :deep(.xterm) { height: 100%; width: 100%; }
|
||||||
|
.terminal-output :deep(.xterm-viewport) { background: transparent !important; }
|
||||||
.terminal-input-bar { border-top: 1px solid rgba(255,255,255,.1); } .terminal-prompt { color: #f7931a; font-size: 20px; } .terminal-input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: white; font: 13px ui-monospace, monospace; }
|
.terminal-input-bar { border-top: 1px solid rgba(255,255,255,.1); } .terminal-prompt { color: #f7931a; font-size: 20px; } .terminal-input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: white; font: 13px ui-monospace, monospace; }
|
||||||
.terminal-status { color: #e8a7a7; font-size: 11px; } .terminal-status.connected { color: #92d6a0; }
|
.terminal-status { color: rgba(255,255,255,.5); font-size: 11px; } .terminal-status.connected { color: #92d6a0; }
|
||||||
.terminal-select, .terminal-button, .terminal-close { min-height: 36px; border-radius: 9px; border: 1px solid rgba(255,255,255,.15); background: rgba(255,255,255,.07); color: white; padding: 0 11px; font-size: 12px; }
|
.terminal-hint { margin-left: auto; color: rgba(255,255,255,.35); font-size: 11px; }
|
||||||
.terminal-button:hover, .terminal-select:hover { background: rgba(255,255,255,.13); } .terminal-primary { background: #f7931a; border-color: #f7931a; color: #17100a; font-weight: 700; } .terminal-close { color: rgba(255,255,255,.65); }
|
.terminal-select, .terminal-icon-button, .terminal-button { min-height: 36px; border-radius: 9px; border: 1px solid rgba(255,255,255,.12); background: rgba(255,255,255,.06); color: white; padding: 0 9px; font-size: 12px; }
|
||||||
|
.terminal-icon-button:hover, .terminal-button:hover, .terminal-select:hover { background: rgba(255,255,255,.13); }
|
||||||
|
.terminal-primary { background: #f7931a; border-color: #f7931a; color: #17100a; font-weight: 700; }
|
||||||
.terminal-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; height: 100%; min-height: 300px; padding: 32px; text-align: center; color: rgba(255,255,255,.55); } .terminal-empty strong { color: white; font-size: 16px; } .terminal-error strong { color: #ffaaa6; }
|
.terminal-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; height: 100%; min-height: 300px; padding: 32px; text-align: center; color: rgba(255,255,255,.55); } .terminal-empty strong { color: white; font-size: 16px; } .terminal-error strong { color: #ffaaa6; }
|
||||||
@media (max-width: 640px) { .terminal-panel { width: calc(100vw - 1rem); height: calc(100vh - 1rem); max-width: calc(100vw - 1rem); max-height: calc(100vh - 1rem); } .terminal-header { align-items: flex-start; flex-direction: column; } .terminal-actions { width: 100%; flex-wrap: wrap; } .terminal-select { flex: 1; min-width: 0; } .terminal-status { display: none; } }
|
@media (max-width: 640px) { .terminal-panel { width: calc(100vw - 1rem); height: calc(100vh - 1rem); max-width: calc(100vw - 1rem); max-height: calc(100vh - 1rem); } .terminal-header { align-items: flex-start; flex-direction: column; } .terminal-actions { width: 100%; flex-wrap: wrap; } .terminal-select { flex: 1; min-width: 0; } .terminal-status { display: none; } }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user