mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-17 13:15:53 +00:00
Implement black theme, mobile improvements, and fit-to-width toggle
- Apply consistent black theme across all components with colored borders - Add animated VibeTunnel logo with rainbow scrolling gradient - Implement comprehensive mobile input controls with Ctrl+Alpha overlay - Add fit-to-width toggle button in session view header with scroll preservation - Enhance mobile experience with proper viewport handling and keyboard positioning - Update button styling to use black backgrounds with colored borders throughout - Resize scroll-to-bottom button for better mobile accessibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
75203f79ab
commit
7c92eb5bdb
10 changed files with 918 additions and 138 deletions
|
|
@ -2,10 +2,16 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
|
||||
<title>VibeTunnel - Terminal Multiplexer</title>
|
||||
<meta name="description" content="Interactive terminal sessions in your browser with real-time streaming and mobile support">
|
||||
|
||||
<!-- PWA and mobile optimizations -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="theme-color" content="#1e1e1e">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
|
||||
|
|
@ -13,6 +19,37 @@
|
|||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css" />
|
||||
<link href="bundle/output.css" rel="stylesheet">
|
||||
|
||||
<!-- Mobile viewport and address bar handling -->
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: calc(var(--vh, 1vh) * 100); /* Dynamic viewport height for mobile */
|
||||
overscroll-behavior-y: none; /* Prevent pull-to-refresh */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Prevent pull-to-refresh only on specific elements */
|
||||
body {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Only disable touch-action on terminal components */
|
||||
vibe-terminal {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/* Ensure app takes full viewport */
|
||||
vibetunnel-app {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Import Maps -->
|
||||
<script type="importmap">
|
||||
{
|
||||
|
|
@ -23,8 +60,36 @@
|
|||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-vs-bg m-0 p-0">
|
||||
<body class="m-0 p-0" style="background: black;">
|
||||
<vibetunnel-app></vibetunnel-app>
|
||||
|
||||
<!-- Mobile viewport height fix -->
|
||||
<script>
|
||||
// Handle dynamic viewport height for mobile browsers
|
||||
function setViewportHeight() {
|
||||
const vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
}
|
||||
|
||||
// Set initial height
|
||||
setViewportHeight();
|
||||
|
||||
// Update on resize and orientation change
|
||||
window.addEventListener('resize', setViewportHeight);
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(setViewportHeight, 100);
|
||||
});
|
||||
|
||||
// Force full-screen behavior
|
||||
window.addEventListener('load', () => {
|
||||
// Scroll to top to hide address bar
|
||||
setTimeout(() => {
|
||||
window.scrollTo(0, 1);
|
||||
setTimeout(() => window.scrollTo(0, 0), 10);
|
||||
}, 10);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="module" src="bundle/client-bundle.js"></script>
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ export class VibeTunnelApp extends LitElement {
|
|||
`
|
||||
)
|
||||
: html`
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="max-w-4xl mx-auto" style="background: black;">
|
||||
<app-header
|
||||
.sessions=${this.sessions}
|
||||
.hideExited=${this.hideExited}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import type { Session } from './session-list.js';
|
||||
import './vibe-logo.js';
|
||||
|
||||
@customElement('app-header')
|
||||
export class AppHeader extends LitElement {
|
||||
|
|
@ -40,11 +41,13 @@ export class AppHeader extends LitElement {
|
|||
}
|
||||
|
||||
return html`
|
||||
<div class="app-header p-4 border-b border-vs-border">
|
||||
<div class="app-header p-4" style="background: black;">
|
||||
<!-- Mobile layout -->
|
||||
<div class="flex flex-col gap-3 sm:hidden">
|
||||
<!-- Centered VibeTunnel title -->
|
||||
<div class="text-vs-user font-mono text-sm text-center">-=[ VibeTunnel ]=-</div>
|
||||
<div class="text-center">
|
||||
<vibe-logo></vibe-logo>
|
||||
</div>
|
||||
|
||||
<!-- Controls row: hide exited on left, buttons on right -->
|
||||
<div class="flex items-center justify-between">
|
||||
|
|
@ -89,16 +92,38 @@ export class AppHeader extends LitElement {
|
|||
${runningSessions.length > 0 && !this.killingAll
|
||||
? html`
|
||||
<button
|
||||
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-2 py-1.5 border-none rounded transition-colors text-xs whitespace-nowrap"
|
||||
class="font-mono px-2 py-1.5 rounded transition-colors text-xs whitespace-nowrap"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #d19a66;"
|
||||
@click=${this.handleKillAll}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#d19a66';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
KILL (${runningSessions.length})
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
<button
|
||||
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-2 py-1.5 border-none rounded transition-colors text-xs whitespace-nowrap"
|
||||
class="font-mono px-2 py-1.5 rounded transition-colors text-xs whitespace-nowrap"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #569cd6;"
|
||||
@click=${this.handleCreateSession}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#569cd6';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
CREATE
|
||||
</button>
|
||||
|
|
@ -108,7 +133,7 @@ export class AppHeader extends LitElement {
|
|||
|
||||
<!-- Desktop layout: single row -->
|
||||
<div class="hidden sm:flex sm:items-center sm:justify-between">
|
||||
<div class="text-vs-user font-mono text-sm">-=[ VibeTunnel ]=-</div>
|
||||
<vibe-logo></vibe-logo>
|
||||
<div class="flex items-center gap-3">
|
||||
<label
|
||||
class="flex items-center gap-2 text-vs-text text-sm cursor-pointer hover:text-vs-accent transition-colors"
|
||||
|
|
@ -150,16 +175,38 @@ export class AppHeader extends LitElement {
|
|||
${runningSessions.length > 0 && !this.killingAll
|
||||
? html`
|
||||
<button
|
||||
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-3 sm:px-4 py-2 border-none rounded transition-colors text-sm whitespace-nowrap"
|
||||
class="font-mono px-3 sm:px-4 py-2 rounded transition-colors text-sm whitespace-nowrap"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #d19a66;"
|
||||
@click=${this.handleKillAll}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#d19a66';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
KILL ALL (${runningSessions.length})
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
<button
|
||||
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-3 sm:px-4 py-2 border-none rounded transition-colors text-sm whitespace-nowrap"
|
||||
class="font-mono px-3 sm:px-4 py-2 rounded transition-colors text-sm whitespace-nowrap"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #569cd6;"
|
||||
@click=${this.handleCreateSession}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#569cd6';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
CREATE SESSION
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -150,16 +150,28 @@ export class FileBrowser extends LitElement {
|
|||
style="z-index: 9999;"
|
||||
>
|
||||
<div
|
||||
class="bg-vs-bg-secondary border border-vs-border font-mono text-sm w-96 h-96 flex flex-col"
|
||||
class="font-mono text-sm w-96 h-96 flex flex-col"
|
||||
style="background: black; border: 1px solid #569cd6; border-radius: 4px;"
|
||||
>
|
||||
<div class="p-4 border-b border-vs-border flex-shrink-0">
|
||||
<div class="p-4 flex-shrink-0" style="border-bottom: 1px solid #444;">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="text-vs-assistant text-sm">Select Directory</div>
|
||||
<div class="text-vs-user text-sm">Select Directory</div>
|
||||
<button
|
||||
class="bg-vs-user text-vs-bg hover:bg-vs-accent font-mono px-2 py-1 text-xs border-none rounded"
|
||||
class="font-mono px-2 py-1 text-xs rounded transition-colors"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #569cd6; border-radius: 4px;"
|
||||
@click=${this.handleCreateFolder}
|
||||
?disabled=${this.loading}
|
||||
title="Create new folder"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#569cd6';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
+ folder
|
||||
</button>
|
||||
|
|
@ -216,7 +228,8 @@ export class FileBrowser extends LitElement {
|
|||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="flex-1 bg-vs-bg border border-vs-border text-vs-text px-2 py-1 text-sm font-mono"
|
||||
class="flex-1 outline-none font-mono px-2 py-1 text-sm"
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px;"
|
||||
placeholder="Folder name"
|
||||
.value=${this.newFolderName}
|
||||
@input=${this.handleFolderNameInput}
|
||||
|
|
@ -224,16 +237,46 @@ export class FileBrowser extends LitElement {
|
|||
?disabled=${this.creating}
|
||||
/>
|
||||
<button
|
||||
class="bg-vs-user text-vs-bg hover:bg-vs-accent font-mono px-2 py-1 text-xs border-none"
|
||||
class="font-mono px-2 py-1 text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #569cd6; border-radius: 4px;"
|
||||
@click=${this.createFolder}
|
||||
?disabled=${this.creating || !this.newFolderName.trim()}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!btn.hasAttribute('disabled')) {
|
||||
btn.style.background = '#569cd6';
|
||||
btn.style.color = 'black';
|
||||
}
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!btn.hasAttribute('disabled')) {
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}
|
||||
}}
|
||||
>
|
||||
${this.creating ? '...' : 'create'}
|
||||
</button>
|
||||
<button
|
||||
class="bg-vs-muted text-vs-bg hover:bg-vs-text font-mono px-2 py-1 text-xs border-none"
|
||||
class="font-mono px-2 py-1 text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #888; border-radius: 4px;"
|
||||
@click=${this.handleCancelCreateFolder}
|
||||
?disabled=${this.creating}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!btn.hasAttribute('disabled')) {
|
||||
btn.style.background = '#888';
|
||||
btn.style.color = 'black';
|
||||
}
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!btn.hasAttribute('disabled')) {
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}
|
||||
}}
|
||||
>
|
||||
cancel
|
||||
</button>
|
||||
|
|
@ -244,14 +287,36 @@ export class FileBrowser extends LitElement {
|
|||
|
||||
<div class="p-4 border-t border-vs-border flex gap-4 justify-end flex-shrink-0">
|
||||
<button
|
||||
class="bg-vs-muted text-vs-bg hover:bg-vs-text font-mono px-4 py-2 border-none"
|
||||
class="font-mono px-4 py-2 transition-colors"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #888; border-radius: 4px;"
|
||||
@click=${this.handleCancel}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#888';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
cancel
|
||||
</button>
|
||||
<button
|
||||
class="bg-vs-user text-vs-bg hover:bg-vs-accent font-mono px-4 py-2 border-none"
|
||||
class="font-mono px-4 py-2 transition-colors"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #569cd6; border-radius: 4px;"
|
||||
@click=${this.handleSelect}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#569cd6';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
select
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -209,8 +209,11 @@ export class SessionCard extends LitElement {
|
|||
@click=${this.handleCardClick}
|
||||
>
|
||||
<!-- Compact Header -->
|
||||
<div class="flex justify-between items-center px-3 py-2 border-b border-vs-border">
|
||||
<div class="text-vs-text text-xs font-mono pr-2 flex-1 min-w-0">
|
||||
<div
|
||||
class="flex justify-between items-center px-3 py-2 border-b border-vs-border"
|
||||
style="background: black;"
|
||||
>
|
||||
<div class="text-xs font-mono pr-2 flex-1 min-w-0" style="color: #569cd6;">
|
||||
<div class="truncate" title="${this.session.name || this.session.command}">
|
||||
${this.session.name || this.session.command}
|
||||
</div>
|
||||
|
|
@ -218,9 +221,24 @@ export class SessionCard extends LitElement {
|
|||
${this.session.status === 'running'
|
||||
? html`
|
||||
<button
|
||||
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-2 py-0.5 border-none text-xs disabled:opacity-50 flex-shrink-0 rounded"
|
||||
class="font-mono px-2 py-0.5 text-xs disabled:opacity-50 flex-shrink-0 rounded transition-colors"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #d19a66;"
|
||||
@click=${this.handleKillClick}
|
||||
?disabled=${this.killing}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!this.killing) {
|
||||
btn.style.background = '#d19a66';
|
||||
btn.style.color = 'black';
|
||||
}
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!this.killing) {
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}
|
||||
}}
|
||||
>
|
||||
${this.killing ? 'killing...' : 'kill'}
|
||||
</button>
|
||||
|
|
@ -253,7 +271,10 @@ export class SessionCard extends LitElement {
|
|||
</div>
|
||||
|
||||
<!-- Compact Footer -->
|
||||
<div class="px-3 py-2 text-vs-muted text-xs border-t border-vs-border">
|
||||
<div
|
||||
class="px-3 py-2 text-vs-muted text-xs border-t border-vs-border"
|
||||
style="background: black;"
|
||||
>
|
||||
<div class="flex justify-between items-center min-w-0">
|
||||
<span class="${this.getStatusColor()} text-xs flex items-center gap-1 flex-shrink-0">
|
||||
<div class="w-2 h-2 rounded-full ${this.getStatusDotColor()}"></div>
|
||||
|
|
|
|||
|
|
@ -227,16 +227,11 @@ export class SessionCreateForm extends LitElement {
|
|||
style="z-index: 9999;"
|
||||
>
|
||||
<div
|
||||
class="bg-vs-bg-secondary border border-vs-border font-mono text-sm w-96 max-w-full mx-4"
|
||||
class="font-mono text-sm w-96 max-w-full mx-4"
|
||||
style="background: black; border: 1px solid #569cd6; border-radius: 4px;"
|
||||
>
|
||||
<div class="p-4 border-b border-vs-border flex justify-between items-center">
|
||||
<div class="text-vs-assistant text-sm">Create New Session</div>
|
||||
<button
|
||||
class="text-vs-muted hover:text-vs-text text-lg leading-none border-none bg-transparent cursor-pointer"
|
||||
@click=${this.handleCancel}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div class="p-4" style="border-bottom: 1px solid #444;">
|
||||
<div class="text-vs-user text-sm">Create New Session</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
|
|
@ -244,7 +239,8 @@ export class SessionCreateForm extends LitElement {
|
|||
<div class="text-vs-text mb-2">Session Name (optional):</div>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full bg-vs-bg text-vs-text border border-vs-border outline-none font-mono px-4 py-2"
|
||||
class="w-full outline-none font-mono px-4 py-2"
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px;"
|
||||
.value=${this.sessionName}
|
||||
@input=${this.handleSessionNameChange}
|
||||
placeholder="My Session"
|
||||
|
|
@ -257,16 +253,28 @@ export class SessionCreateForm extends LitElement {
|
|||
<div class="flex gap-4">
|
||||
<input
|
||||
type="text"
|
||||
class="flex-1 bg-vs-bg text-vs-text border border-vs-border outline-none font-mono px-4 py-2"
|
||||
class="flex-1 outline-none font-mono px-4 py-2"
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px;"
|
||||
.value=${this.workingDir}
|
||||
@input=${this.handleWorkingDirChange}
|
||||
placeholder="~/"
|
||||
?disabled=${this.disabled || this.isCreating}
|
||||
/>
|
||||
<button
|
||||
class="bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-4 py-2 border-none"
|
||||
class="font-mono px-4 py-2 transition-colors"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #569cd6; border-radius: 4px;"
|
||||
@click=${this.handleBrowse}
|
||||
?disabled=${this.disabled || this.isCreating}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#569cd6';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
browse
|
||||
</button>
|
||||
|
|
@ -277,7 +285,8 @@ export class SessionCreateForm extends LitElement {
|
|||
<div class="text-vs-text mb-2">Command:</div>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full bg-vs-bg text-vs-text border border-vs-border outline-none font-mono px-4 py-2"
|
||||
class="w-full outline-none font-mono px-4 py-2"
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px;"
|
||||
.value=${this.command}
|
||||
@input=${this.handleCommandChange}
|
||||
@keydown=${(e: KeyboardEvent) => e.key === 'Enter' && this.handleCreate()}
|
||||
|
|
@ -288,19 +297,45 @@ export class SessionCreateForm extends LitElement {
|
|||
|
||||
<div class="flex gap-4 justify-end">
|
||||
<button
|
||||
class="bg-vs-muted text-vs-bg hover:bg-vs-text font-mono px-4 py-2 border-none"
|
||||
class="font-mono px-4 py-2 transition-colors"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #888; border-radius: 4px;"
|
||||
@click=${this.handleCancel}
|
||||
?disabled=${this.isCreating}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#888';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
cancel
|
||||
</button>
|
||||
<button
|
||||
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-2 border-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-vs-user"
|
||||
class="font-mono px-4 py-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #569cd6; border-radius: 4px;"
|
||||
@click=${this.handleCreate}
|
||||
?disabled=${this.disabled ||
|
||||
this.isCreating ||
|
||||
!this.workingDir.trim() ||
|
||||
!this.command.trim()}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!btn.hasAttribute('disabled')) {
|
||||
btn.style.background = '#569cd6';
|
||||
btn.style.color = 'black';
|
||||
}
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!btn.hasAttribute('disabled')) {
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}
|
||||
}}
|
||||
>
|
||||
${this.isCreating ? 'creating...' : 'create'}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -74,15 +74,30 @@ export class SessionList extends LitElement {
|
|||
: this.sessions;
|
||||
|
||||
return html`
|
||||
<div class="font-mono text-sm p-4">
|
||||
<div class="font-mono text-sm p-4" style="background: black;">
|
||||
<!-- Controls -->
|
||||
${!this.hideExited && this.sessions.filter((s) => s.status === 'exited').length > 0
|
||||
? html`
|
||||
<div class="mb-4">
|
||||
<button
|
||||
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-4 py-2 border-none rounded transition-colors disabled:opacity-50"
|
||||
class="font-mono px-4 py-2 rounded transition-colors disabled:opacity-50"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #d19a66;"
|
||||
@click=${this.handleCleanupExited}
|
||||
?disabled=${this.cleaningExited}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!this.cleaningExited) {
|
||||
btn.style.background = '#d19a66';
|
||||
btn.style.color = 'black';
|
||||
}
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!this.cleaningExited) {
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}
|
||||
}}
|
||||
>
|
||||
${this.cleaningExited ? '[~] CLEANING...' : 'CLEAN EXITED'}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ export class SessionView extends LitElement {
|
|||
@state() private loadingFrame = 0;
|
||||
@state() private terminalCols = 0;
|
||||
@state() private terminalRows = 0;
|
||||
@state() private showCtrlAlpha = false;
|
||||
@state() private terminalFitHorizontally = false;
|
||||
|
||||
private loadingInterval: number | null = null;
|
||||
private keyboardListenerAdded = false;
|
||||
|
|
@ -88,6 +90,11 @@ export class SessionView extends LitElement {
|
|||
navigator.userAgent
|
||||
);
|
||||
|
||||
// Hide mobile address bar when entering session view
|
||||
if (this.isMobile) {
|
||||
this.hideAddressBar();
|
||||
}
|
||||
|
||||
// Only add listeners if not already added
|
||||
if (!this.isMobile && !this.keyboardListenerAdded) {
|
||||
document.addEventListener('keydown', this.keyboardHandler);
|
||||
|
|
@ -154,6 +161,11 @@ export class SessionView extends LitElement {
|
|||
const terminalElement = this.querySelector('vibe-terminal') as Terminal;
|
||||
if (terminalElement) {
|
||||
this.initializeTerminal();
|
||||
|
||||
// Hide address bar again after terminal is ready
|
||||
if (this.isMobile) {
|
||||
setTimeout(() => this.hideAddressBar(), 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -344,6 +356,24 @@ export class SessionView extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
private hideAddressBar() {
|
||||
// Trigger address bar hiding on mobile
|
||||
if (window.innerHeight !== window.outerHeight) {
|
||||
// Multiple attempts with different timing to ensure it works
|
||||
setTimeout(() => {
|
||||
window.scrollTo(0, 1);
|
||||
setTimeout(() => {
|
||||
window.scrollTo(0, 0);
|
||||
// Force another attempt after a brief delay
|
||||
setTimeout(() => {
|
||||
window.scrollTo(0, 1);
|
||||
setTimeout(() => window.scrollTo(0, 0), 50);
|
||||
}, 100);
|
||||
}, 50);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
private handleBack() {
|
||||
window.location.search = '';
|
||||
}
|
||||
|
|
@ -558,7 +588,8 @@ export class SessionView extends LitElement {
|
|||
|
||||
try {
|
||||
// Add enter key at the end to execute the command
|
||||
await this.sendInputText(textToSend + '\n');
|
||||
await this.sendInputText(textToSend);
|
||||
await this.sendInputText('enter');
|
||||
|
||||
// Clear both the reactive property and textarea
|
||||
this.mobileInputText = '';
|
||||
|
|
@ -581,6 +612,33 @@ export class SessionView extends LitElement {
|
|||
await this.sendInputText(key);
|
||||
}
|
||||
|
||||
private handleCtrlAlphaToggle() {
|
||||
this.showCtrlAlpha = !this.showCtrlAlpha;
|
||||
}
|
||||
|
||||
private async handleCtrlKey(letter: string) {
|
||||
// Convert letter to control character (A=1, B=2, ..., Z=26)
|
||||
const controlCode = String.fromCharCode(letter.charCodeAt(0) - 64);
|
||||
await this.sendInputText(controlCode);
|
||||
this.showCtrlAlpha = false; // Close overlay after sending
|
||||
}
|
||||
|
||||
private handleCtrlAlphaBackdrop(e: Event) {
|
||||
if (e.target === e.currentTarget) {
|
||||
this.showCtrlAlpha = false;
|
||||
}
|
||||
}
|
||||
|
||||
private handleTerminalFitToggle() {
|
||||
this.terminalFitHorizontally = !this.terminalFitHorizontally;
|
||||
// Find the terminal component and call its handleFitToggle method
|
||||
const terminal = this.querySelector('vibe-terminal') as any;
|
||||
if (terminal && terminal.handleFitToggle) {
|
||||
// Use the terminal's own toggle method which handles scroll position correctly
|
||||
terminal.handleFitToggle();
|
||||
}
|
||||
}
|
||||
|
||||
private handlePasteEvent = async (e: ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -736,6 +794,30 @@ export class SessionView extends LitElement {
|
|||
return frames[this.loadingFrame % frames.length];
|
||||
}
|
||||
|
||||
private getStatusText(): string {
|
||||
if (!this.session) return '';
|
||||
if ('waiting' in this.session && this.session.waiting) {
|
||||
return 'waiting';
|
||||
}
|
||||
return this.session.status;
|
||||
}
|
||||
|
||||
private getStatusColor(): string {
|
||||
if (!this.session) return 'text-vs-muted';
|
||||
if ('waiting' in this.session && this.session.waiting) {
|
||||
return 'text-vs-muted';
|
||||
}
|
||||
return this.session.status === 'running' ? 'text-vs-user' : 'text-vs-warning';
|
||||
}
|
||||
|
||||
private getStatusDotColor(): string {
|
||||
if (!this.session) return 'bg-gray-500';
|
||||
if ('waiting' in this.session && this.session.waiting) {
|
||||
return 'bg-gray-500';
|
||||
}
|
||||
return this.session.status === 'running' ? 'bg-green-500' : 'bg-orange-500';
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.session) {
|
||||
return html` <div class="p-4 text-vs-muted">No session selected</div> `;
|
||||
|
|
@ -760,12 +842,24 @@ export class SessionView extends LitElement {
|
|||
>
|
||||
<!-- Compact Header -->
|
||||
<div
|
||||
class="flex items-center justify-between px-3 py-2 border-b border-vs-border bg-vs-bg-secondary text-sm min-w-0"
|
||||
class="flex items-center justify-between px-3 py-2 border-b border-vs-border text-sm min-w-0"
|
||||
style="background: black;"
|
||||
>
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<button
|
||||
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-2 py-1 border-none rounded transition-colors text-xs flex-shrink-0"
|
||||
class="font-mono px-2 py-1 rounded transition-colors text-xs flex-shrink-0"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #569cd6;"
|
||||
@click=${this.handleBack}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#569cd6';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
BACK
|
||||
</button>
|
||||
|
|
@ -776,28 +870,43 @@ export class SessionView extends LitElement {
|
|||
>
|
||||
${this.session.name || this.session.command}
|
||||
</div>
|
||||
<div
|
||||
class="text-vs-muted text-xs overflow-x-auto scrollbar-thin scrollbar-thumb-vs-border scrollbar-track-transparent whitespace-nowrap"
|
||||
title="${this.session.workingDir}"
|
||||
>
|
||||
${this.session.workingDir}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-0 text-xs flex-shrink-0 ml-2">
|
||||
<span class="${this.session.status === 'running' ? 'text-vs-user' : 'text-vs-warning'}">
|
||||
${this.session.status.toUpperCase()}
|
||||
</span>
|
||||
${this.terminalCols > 0 && this.terminalRows > 0
|
||||
? html`
|
||||
<span
|
||||
class="text-vs-muted text-xs opacity-60"
|
||||
style="font-size: 10px; line-height: 1;"
|
||||
>
|
||||
${this.terminalCols}×${this.terminalRows}
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
<div class="flex items-center gap-2 text-xs flex-shrink-0 ml-2">
|
||||
<div class="flex flex-col items-end gap-0">
|
||||
<span class="${this.getStatusColor()} text-xs flex items-center gap-1">
|
||||
<div class="w-2 h-2 rounded-full ${this.getStatusDotColor()}"></div>
|
||||
${this.getStatusText().toUpperCase()}
|
||||
</span>
|
||||
${this.terminalCols > 0 && this.terminalRows > 0
|
||||
? html`
|
||||
<span
|
||||
class="text-vs-muted text-xs opacity-60"
|
||||
style="font-size: 10px; line-height: 1;"
|
||||
>
|
||||
${this.terminalCols}×${this.terminalRows}
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
<button
|
||||
class="font-mono text-lg transition-colors flex-shrink-0"
|
||||
style="background: transparent; color: ${this.terminalFitHorizontally ? '#569cd6' : '#d4d4d4'}; border: none; padding: 4px;"
|
||||
@click=${this.handleTerminalFitToggle}
|
||||
title="Toggle fit to width"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.color = '#569cd6';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.color = this.terminalFitHorizontally ? '#569cd6' : '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
${this.terminalFitHorizontally
|
||||
? html`<span>←</span> <span>→</span>`
|
||||
: html`<span>→</span> <span>←</span>`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -834,30 +943,74 @@ export class SessionView extends LitElement {
|
|||
<!-- Mobile Input Controls -->
|
||||
${this.isMobile && !this.showMobileInput
|
||||
? html`
|
||||
<div class="flex-shrink-0 p-4 bg-vs-bg">
|
||||
<div class="flex-shrink-0 p-4" style="background: black;">
|
||||
<!-- First row: Arrow keys -->
|
||||
<div class="flex gap-2 mb-2">
|
||||
<button
|
||||
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
|
||||
class="flex-1 font-mono px-3 py-2 text-sm transition-all cursor-pointer"
|
||||
@click=${() => this.handleSpecialKey('arrow_up')}
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.9)';
|
||||
btn.style.borderColor = '#666';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
btn.style.borderColor = '#444';
|
||||
}}
|
||||
>
|
||||
<span class="text-xl">↑</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
|
||||
class="flex-1 font-mono px-3 py-2 text-sm transition-all cursor-pointer"
|
||||
@click=${() => this.handleSpecialKey('arrow_down')}
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.9)';
|
||||
btn.style.borderColor = '#666';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
btn.style.borderColor = '#444';
|
||||
}}
|
||||
>
|
||||
<span class="text-xl">↓</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
|
||||
class="flex-1 font-mono px-3 py-2 text-sm transition-all cursor-pointer"
|
||||
@click=${() => this.handleSpecialKey('arrow_left')}
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.9)';
|
||||
btn.style.borderColor = '#666';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
btn.style.borderColor = '#444';
|
||||
}}
|
||||
>
|
||||
<span class="text-xl">←</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
|
||||
class="flex-1 font-mono px-3 py-2 text-sm transition-all cursor-pointer"
|
||||
@click=${() => this.handleSpecialKey('arrow_right')}
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.9)';
|
||||
btn.style.borderColor = '#666';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
btn.style.borderColor = '#444';
|
||||
}}
|
||||
>
|
||||
<span class="text-xl">→</span>
|
||||
</button>
|
||||
|
|
@ -866,34 +1019,89 @@ export class SessionView extends LitElement {
|
|||
<!-- Second row: Special keys -->
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
|
||||
class="font-mono text-sm transition-all cursor-pointer w-16"
|
||||
@click=${() => this.handleSpecialKey('\t')}
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px; padding: 8px 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.9)';
|
||||
btn.style.borderColor = '#666';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
btn.style.borderColor = '#444';
|
||||
}}
|
||||
>
|
||||
<span class="text-xl">⇥</span>
|
||||
TAB
|
||||
</button>
|
||||
<button
|
||||
class="bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
|
||||
class="font-mono text-sm transition-all cursor-pointer w-16"
|
||||
@click=${() => this.handleSpecialKey('enter')}
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px; padding: 8px 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.9)';
|
||||
btn.style.borderColor = '#666';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
btn.style.borderColor = '#444';
|
||||
}}
|
||||
>
|
||||
<span class="text-xl">⏎</span>
|
||||
ENTER
|
||||
</button>
|
||||
<button
|
||||
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
|
||||
class="font-mono text-sm transition-all cursor-pointer w-16"
|
||||
@click=${() => this.handleSpecialKey('escape')}
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px; padding: 8px 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.9)';
|
||||
btn.style.borderColor = '#666';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
btn.style.borderColor = '#444';
|
||||
}}
|
||||
>
|
||||
ESC
|
||||
</button>
|
||||
<button
|
||||
class="bg-vs-error text-vs-text hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
|
||||
@click=${() => this.handleSpecialKey('\x03')}
|
||||
class="font-mono text-sm transition-all cursor-pointer w-16"
|
||||
@click=${this.handleCtrlAlphaToggle}
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px; padding: 8px 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.9)';
|
||||
btn.style.borderColor = '#666';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
btn.style.borderColor = '#444';
|
||||
}}
|
||||
>
|
||||
^C
|
||||
CTRL
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
|
||||
class="flex-1 font-mono px-3 py-2 text-sm transition-all cursor-pointer"
|
||||
@click=${this.handleMobileInputToggle}
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.9)';
|
||||
btn.style.borderColor = '#666';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
btn.style.borderColor = '#444';
|
||||
}}
|
||||
>
|
||||
TYPE
|
||||
ABC123
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -904,80 +1112,211 @@ export class SessionView extends LitElement {
|
|||
${this.isMobile && this.showMobileInput
|
||||
? html`
|
||||
<div
|
||||
class="fixed inset-0 bg-vs-bg-secondary bg-opacity-95 z-50 flex flex-col"
|
||||
style="height: 100vh; height: 100dvh;"
|
||||
class="fixed inset-0 z-50 flex flex-col"
|
||||
style="background: rgba(0, 0, 0, 0.8);"
|
||||
@click=${(e: Event) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
this.showMobileInput = false;
|
||||
}
|
||||
}}
|
||||
@touchstart=${this.touchStartHandler}
|
||||
@touchend=${this.touchEndHandler}
|
||||
>
|
||||
<!-- Input Header -->
|
||||
<!-- Spacer to push content up above keyboard -->
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between p-4 border-b border-vs-border flex-shrink-0"
|
||||
class="font-mono text-sm mx-4 mb-4 flex flex-col"
|
||||
style="background: black; border: 1px solid #569cd6; border-radius: 8px; transform: translateY(-120px);"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
>
|
||||
<div class="text-vs-text font-mono text-sm">Terminal Input</div>
|
||||
<button
|
||||
class="text-vs-muted hover:text-vs-text text-lg leading-none border-none bg-transparent cursor-pointer"
|
||||
@click=${this.handleMobileInputToggle}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Input Area with dynamic height -->
|
||||
<div class="flex-1 p-4 flex flex-col min-h-0">
|
||||
<div class="text-vs-muted text-sm mb-2 flex-shrink-0">
|
||||
Type your command(s) below. Supports multiline input.
|
||||
<!-- Input Area -->
|
||||
<div class="p-4 flex flex-col">
|
||||
<textarea
|
||||
id="mobile-input-textarea"
|
||||
class="w-full font-mono text-sm resize-none outline-none"
|
||||
placeholder="Type your command here..."
|
||||
.value=${this.mobileInputText}
|
||||
@input=${this.handleMobileInputChange}
|
||||
@click=${(e: Event) => {
|
||||
const textarea = e.target as HTMLTextAreaElement;
|
||||
setTimeout(() => {
|
||||
textarea.focus();
|
||||
}, 10);
|
||||
}}
|
||||
@keydown=${(e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.handleMobileInputSend();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
this.showMobileInput = false;
|
||||
}
|
||||
}}
|
||||
style="height: 120px; background: black; color: #d4d4d4; border: none; padding: 12px;"
|
||||
></textarea>
|
||||
</div>
|
||||
<textarea
|
||||
id="mobile-input-textarea"
|
||||
class="flex-1 bg-vs-bg text-vs-text border border-vs-border font-mono text-sm p-4 resize-none outline-none"
|
||||
placeholder="Enter your command here..."
|
||||
.value=${this.mobileInputText}
|
||||
@input=${this.handleMobileInputChange}
|
||||
@click=${(e: Event) => {
|
||||
const textarea = e.target as HTMLTextAreaElement;
|
||||
// Ensure keyboard shows when clicking the textarea
|
||||
setTimeout(() => {
|
||||
textarea.focus();
|
||||
}, 10);
|
||||
}}
|
||||
@focus=${() => {
|
||||
// Ensure keyboard adjustment when textarea gains focus
|
||||
this.adjustTextareaForKeyboard();
|
||||
}}
|
||||
@keydown=${(e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.handleMobileInputSend();
|
||||
}
|
||||
}}
|
||||
style="min-height: 120px; margin-bottom: 8px;"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Controls - Fixed above keyboard -->
|
||||
<div
|
||||
id="mobile-controls"
|
||||
class="fixed bottom-0 left-0 right-0 p-4 border-t border-vs-border bg-vs-bg-secondary z-60"
|
||||
style="padding-bottom: max(1rem, env(safe-area-inset-bottom)); transform: translateY(0px);"
|
||||
>
|
||||
<!-- Send Buttons Row -->
|
||||
<div class="flex gap-2 mb-3">
|
||||
<!-- Controls -->
|
||||
<div class="p-4 flex gap-2" style="border-top: 1px solid #444;">
|
||||
<button
|
||||
class="flex-1 bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-3 border-none rounded transition-colors text-sm font-bold"
|
||||
class="font-mono px-3 py-2 text-xs transition-colors"
|
||||
@click=${() => (this.showMobileInput = false)}
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #888; border-radius: 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#888';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
CANCEL
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 font-mono px-3 py-2 text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click=${this.handleMobileInputSendOnly}
|
||||
?disabled=${!this.mobileInputText.trim()}
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #888; border-radius: 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!btn.hasAttribute('disabled')) {
|
||||
btn.style.background = '#888';
|
||||
btn.style.color = 'black';
|
||||
}
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!btn.hasAttribute('disabled')) {
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}
|
||||
}}
|
||||
>
|
||||
SEND
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-4 py-3 border-none rounded transition-colors text-sm font-bold"
|
||||
class="flex-1 font-mono px-3 py-2 text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click=${this.handleMobileInputSend}
|
||||
?disabled=${!this.mobileInputText.trim()}
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #569cd6; border-radius: 4px;"
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!btn.hasAttribute('disabled')) {
|
||||
btn.style.background = '#569cd6';
|
||||
btn.style.color = 'black';
|
||||
}
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
if (!btn.hasAttribute('disabled')) {
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}
|
||||
}}
|
||||
>
|
||||
SEND + ENTER
|
||||
SEND + ⏎
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
|
||||
<div class="text-vs-muted text-xs text-center">
|
||||
SEND: text only • SEND + ENTER: text with enter key
|
||||
<!-- Ctrl+Alpha Overlay -->
|
||||
${this.isMobile && this.showCtrlAlpha
|
||||
? html`
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="background: rgba(0, 0, 0, 0.8);"
|
||||
@click=${this.handleCtrlAlphaBackdrop}
|
||||
>
|
||||
<div
|
||||
class="font-mono text-sm m-4 max-w-sm w-full"
|
||||
style="background: black; border: 1px solid #569cd6; border-radius: 8px; padding: 20px;"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
>
|
||||
<div class="text-vs-user text-center mb-4 font-bold">Ctrl + Key</div>
|
||||
|
||||
<!-- Grid of A-Z buttons -->
|
||||
<div class="grid grid-cols-6 gap-2 mb-4">
|
||||
${[
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
'O',
|
||||
'P',
|
||||
'Q',
|
||||
'R',
|
||||
'S',
|
||||
'T',
|
||||
'U',
|
||||
'V',
|
||||
'W',
|
||||
'X',
|
||||
'Y',
|
||||
'Z',
|
||||
].map(
|
||||
(letter) => html`
|
||||
<button
|
||||
class="font-mono text-xs transition-all cursor-pointer aspect-square flex items-center justify-center"
|
||||
style="background: rgba(0, 0, 0, 0.8); color: #d4d4d4; border: 1px solid #444; border-radius: 4px;"
|
||||
@click=${() => this.handleCtrlKey(letter)}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#569cd6';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
${letter}
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
||||
<!-- Common shortcuts info -->
|
||||
<div class="text-xs text-vs-muted text-center mb-4">
|
||||
<div>Common: C=interrupt, X=exit, O=save, W=search</div>
|
||||
</div>
|
||||
|
||||
<!-- Close button -->
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
class="font-mono px-4 py-2 text-sm transition-all cursor-pointer"
|
||||
style="background: black; color: #d4d4d4; border: 1px solid #888; border-radius: 4px;"
|
||||
@click=${() => (this.showCtrlAlpha = false)}
|
||||
@mouseover=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = '#888';
|
||||
btn.style.color = 'black';
|
||||
}}
|
||||
@mouseout=${(e: Event) => {
|
||||
const btn = e.target as HTMLElement;
|
||||
btn.style.background = 'black';
|
||||
btn.style.color = '#d4d4d4';
|
||||
}}
|
||||
>
|
||||
CLOSE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1033,6 +1033,53 @@ export class Terminal extends LitElement {
|
|||
this.requestUpdate();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle fit to width toggle
|
||||
*/
|
||||
public handleFitToggle = () => {
|
||||
if (!this.terminal || !this.container) {
|
||||
this.fitHorizontally = !this.fitHorizontally;
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store current logical scroll position before toggling
|
||||
const buffer = this.terminal.buffer.active;
|
||||
const currentLineHeight = this.fontSize * 1.2;
|
||||
const currentScrollLines = currentLineHeight > 0 ? this.viewportY / currentLineHeight : 0;
|
||||
const wasAtBottom = this.isScrolledToBottom();
|
||||
|
||||
// Store original font size when entering fit mode
|
||||
if (!this.fitHorizontally) {
|
||||
this.originalFontSize = this.fontSize;
|
||||
}
|
||||
|
||||
// Toggle the mode
|
||||
this.fitHorizontally = !this.fitHorizontally;
|
||||
|
||||
// Restore original font size when exiting fit mode
|
||||
if (!this.fitHorizontally) {
|
||||
this.fontSize = this.originalFontSize;
|
||||
}
|
||||
|
||||
// Recalculate fit
|
||||
this.fitTerminal();
|
||||
|
||||
// Restore scroll position - prioritize staying at bottom if we were there
|
||||
if (wasAtBottom) {
|
||||
// Force scroll to bottom with new dimensions
|
||||
this.scrollToBottom();
|
||||
} else {
|
||||
// Restore logical scroll position for non-bottom positions
|
||||
const newLineHeight = this.fontSize * 1.2;
|
||||
const maxScrollPixels = Math.max(0, (buffer.length - this.actualRows) * newLineHeight);
|
||||
const newViewportY = currentScrollLines * newLineHeight;
|
||||
this.viewportY = Math.max(0, Math.min(maxScrollPixels, newViewportY));
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<style>
|
||||
|
|
@ -1135,17 +1182,17 @@ export class Terminal extends LitElement {
|
|||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #d4d4d4;
|
||||
font-size: 16px;
|
||||
font-size: 24px;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
z-index: 10;
|
||||
|
|
@ -1191,6 +1238,41 @@ export class Terminal extends LitElement {
|
|||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.fit-toggle {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #d4d4d4;
|
||||
font-size: 20px;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.fit-toggle:hover {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-color: #666;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.fit-toggle:active {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
.fit-toggle.active {
|
||||
border-color: #569cd6;
|
||||
color: #569cd6;
|
||||
}
|
||||
</style>
|
||||
<div style="position: relative; width: 100%; height: 100%;">
|
||||
<div id="terminal-container" class="terminal-container w-full h-full overflow-hidden"></div>
|
||||
|
|
|
|||
111
web/src/client/components/vibe-logo.ts
Normal file
111
web/src/client/components/vibe-logo.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('vibe-logo')
|
||||
export class VibeLogo extends LitElement {
|
||||
// Disable shadow DOM to use Tailwind
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@state() private frame = 0;
|
||||
private animationInterval: number | null = null;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.startAnimation();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.stopAnimation();
|
||||
}
|
||||
|
||||
private startAnimation() {
|
||||
this.animationInterval = window.setInterval(() => {
|
||||
this.frame = (this.frame + 1) % 12; // 12 frames for smooth animation
|
||||
this.requestUpdate();
|
||||
}, 200); // Change frame every 200ms
|
||||
}
|
||||
|
||||
private stopAnimation() {
|
||||
if (this.animationInterval) {
|
||||
clearInterval(this.animationInterval);
|
||||
this.animationInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private getLogoFrame(): string {
|
||||
const frames = [
|
||||
'░░░▒▒▓▓█ VibeTunnel █▓▓▒▒░░░',
|
||||
'░░▒▒▓▓█░ VibeTunnel ░█▓▓▒▒░░',
|
||||
'░▒▒▓▓█░░ VibeTunnel ░░█▓▓▒▒░',
|
||||
'▒▒▓▓█░░░ VibeTunnel ░░░█▓▓▒▒',
|
||||
'▒▓▓█░░░░ VibeTunnel ░░░░█▓▓▒',
|
||||
'▓▓█░░░░░ VibeTunnel ░░░░░█▓▓',
|
||||
'▓█░░░░░░ VibeTunnel ░░░░░░█▓',
|
||||
'█░░░░░░░ VibeTunnel ░░░░░░░█',
|
||||
'░░░░░░░█ VibeTunnel █░░░░░░░',
|
||||
'░░░░░░█▓ VibeTunnel ▓█░░░░░░',
|
||||
'░░░░░█▓▓ VibeTunnel ▓▓█░░░░░',
|
||||
'░░░░█▓▓▒ VibeTunnel ▒▓▓█░░░░',
|
||||
];
|
||||
|
||||
return frames[this.frame];
|
||||
}
|
||||
|
||||
private getRainbowColors() {
|
||||
return [
|
||||
'#ff0000',
|
||||
'#ff4500',
|
||||
'#ff8c00',
|
||||
'#ffd700',
|
||||
'#9acd32',
|
||||
'#00ff00',
|
||||
'#00ffff',
|
||||
'#0080ff',
|
||||
'#8000ff',
|
||||
'#ff00ff',
|
||||
'#ff1493',
|
||||
'#ff69b4',
|
||||
'#ffc0cb',
|
||||
'#ffb6c1',
|
||||
'#ffa0b4',
|
||||
'#ff8fa3',
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const frame = this.getLogoFrame();
|
||||
const colors = this.getRainbowColors();
|
||||
|
||||
// Parse the frame to apply rainbow colors
|
||||
const parts = frame.split(' VibeTunnel ');
|
||||
const leftPart = parts[0];
|
||||
const rightPart = parts[1];
|
||||
|
||||
const coloredLeft = leftPart
|
||||
.split('')
|
||||
.map((char, i) =>
|
||||
char === ' ' ? ' ' : html`<span style="color: ${colors[i % colors.length]};">${char}</span>`
|
||||
);
|
||||
|
||||
const coloredRight = rightPart
|
||||
.split('')
|
||||
.map((char, i) =>
|
||||
char === ' '
|
||||
? ' '
|
||||
: html`<span style="color: ${colors[(leftPart.length - 1 - i) % colors.length]};"
|
||||
>${char}</span
|
||||
>`
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="font-mono text-sm select-none leading-tight text-center">
|
||||
<pre
|
||||
class="whitespace-pre"
|
||||
>${coloredLeft} <span class="text-vs-user">VibeTunnel</span> ${coloredRight}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue