mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-09 11:55:53 +00:00
- Create renderer-entry.ts to bundle both Renderer and XTermRenderer - Add bundle:renderer script to generate public/bundle/renderer.js - Update all test files to import from ../bundle/renderer.js - Remove all unpkg/CDN XTerm.js script imports - Tests now use bundled dependencies for faster loading 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
659 lines
No EOL
27 KiB
JavaScript
659 lines
No EOL
27 KiB
JavaScript
"use strict";
|
||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||
};
|
||
var __metadata = (this && this.__metadata) || function (k, v) {
|
||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||
};
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.SessionView = void 0;
|
||
const lit_1 = require("lit");
|
||
const decorators_js_1 = require("lit/decorators.js");
|
||
let SessionView = class SessionView extends lit_1.LitElement {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.session = null;
|
||
this.connected = false;
|
||
this.player = null;
|
||
this.sessionStatusInterval = null;
|
||
this.showMobileInput = false;
|
||
this.mobileInputText = '';
|
||
this.isMobile = false;
|
||
this.touchStartX = 0;
|
||
this.touchStartY = 0;
|
||
this.keyboardHandler = (e) => {
|
||
if (!this.session)
|
||
return;
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
this.handleKeyboardInput(e);
|
||
};
|
||
this.touchStartHandler = (e) => {
|
||
if (!this.isMobile)
|
||
return;
|
||
const touch = e.touches[0];
|
||
this.touchStartX = touch.clientX;
|
||
this.touchStartY = touch.clientY;
|
||
};
|
||
this.touchEndHandler = (e) => {
|
||
if (!this.isMobile)
|
||
return;
|
||
const touch = e.changedTouches[0];
|
||
const touchEndX = touch.clientX;
|
||
const touchEndY = touch.clientY;
|
||
const deltaX = touchEndX - this.touchStartX;
|
||
const deltaY = touchEndY - this.touchStartY;
|
||
// Check for horizontal swipe from left edge (back gesture)
|
||
const isSwipeRight = deltaX > 100;
|
||
const isVerticallyStable = Math.abs(deltaY) < 100;
|
||
const startedFromLeftEdge = this.touchStartX < 50;
|
||
if (isSwipeRight && isVerticallyStable && startedFromLeftEdge) {
|
||
// Trigger back navigation
|
||
this.handleBack();
|
||
}
|
||
};
|
||
}
|
||
// Disable shadow DOM to use Tailwind
|
||
createRenderRoot() {
|
||
return this;
|
||
}
|
||
connectedCallback() {
|
||
super.connectedCallback();
|
||
this.connected = true;
|
||
// Detect mobile device
|
||
this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||
window.innerWidth <= 768;
|
||
// Add global keyboard event listener only for desktop
|
||
if (!this.isMobile) {
|
||
document.addEventListener('keydown', this.keyboardHandler);
|
||
}
|
||
else {
|
||
// Add touch event listeners for mobile swipe gestures
|
||
document.addEventListener('touchstart', this.touchStartHandler, { passive: true });
|
||
document.addEventListener('touchend', this.touchEndHandler, { passive: true });
|
||
}
|
||
// Start polling session status
|
||
this.startSessionStatusPolling();
|
||
}
|
||
disconnectedCallback() {
|
||
super.disconnectedCallback();
|
||
this.connected = false;
|
||
// Remove global keyboard event listener
|
||
if (!this.isMobile) {
|
||
document.removeEventListener('keydown', this.keyboardHandler);
|
||
}
|
||
else {
|
||
// Remove touch event listeners
|
||
document.removeEventListener('touchstart', this.touchStartHandler);
|
||
document.removeEventListener('touchend', this.touchEndHandler);
|
||
}
|
||
// Stop polling session status
|
||
this.stopSessionStatusPolling();
|
||
// Cleanup player if exists
|
||
if (this.player) {
|
||
this.player = null;
|
||
}
|
||
}
|
||
updated(changedProperties) {
|
||
super.updated(changedProperties);
|
||
if (changedProperties.has('session') && this.session) {
|
||
// Use setTimeout to ensure DOM is rendered first
|
||
setTimeout(() => {
|
||
this.createInteractiveTerminal();
|
||
}, 10);
|
||
}
|
||
}
|
||
createInteractiveTerminal() {
|
||
if (!this.session)
|
||
return;
|
||
const terminalElement = this.querySelector('#interactive-terminal');
|
||
if (terminalElement && window.AsciinemaPlayer) {
|
||
try {
|
||
// For ended sessions, use snapshot instead of stream to avoid reloading
|
||
const url = this.session.status === 'exited'
|
||
? `/api/sessions/${this.session.id}/snapshot`
|
||
: `/api/sessions/${this.session.id}/stream`;
|
||
const config = this.session.status === 'exited'
|
||
? { url } // Static snapshot
|
||
: { driver: "eventsource", url }; // Live stream
|
||
this.player = window.AsciinemaPlayer.create(config, terminalElement, {
|
||
autoPlay: true,
|
||
loop: false,
|
||
controls: false,
|
||
fit: 'both',
|
||
terminalFontSize: '12px',
|
||
idleTimeLimit: 0.5,
|
||
preload: true,
|
||
poster: 'npt:999999'
|
||
});
|
||
// Disable focus outline and fullscreen functionality
|
||
if (this.player && this.player.el) {
|
||
// Remove focus outline
|
||
this.player.el.style.outline = 'none';
|
||
this.player.el.style.border = 'none';
|
||
// Disable fullscreen hotkey by removing tabindex and preventing focus
|
||
this.player.el.removeAttribute('tabindex');
|
||
this.player.el.style.pointerEvents = 'none';
|
||
// Find the terminal element and make it non-focusable
|
||
const terminal = this.player.el.querySelector('.ap-terminal, .ap-screen, pre');
|
||
if (terminal) {
|
||
terminal.removeAttribute('tabindex');
|
||
terminal.style.outline = 'none';
|
||
}
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('Error creating interactive terminal:', error);
|
||
}
|
||
}
|
||
}
|
||
async handleKeyboardInput(e) {
|
||
if (!this.session)
|
||
return;
|
||
let inputText = '';
|
||
// Handle special keys
|
||
switch (e.key) {
|
||
case 'Enter':
|
||
inputText = 'enter';
|
||
break;
|
||
case 'Escape':
|
||
inputText = 'escape';
|
||
break;
|
||
case 'ArrowUp':
|
||
inputText = 'arrow_up';
|
||
break;
|
||
case 'ArrowDown':
|
||
inputText = 'arrow_down';
|
||
break;
|
||
case 'ArrowLeft':
|
||
inputText = 'arrow_left';
|
||
break;
|
||
case 'ArrowRight':
|
||
inputText = 'arrow_right';
|
||
break;
|
||
case 'Tab':
|
||
inputText = '\t';
|
||
break;
|
||
case 'Backspace':
|
||
inputText = '\b';
|
||
break;
|
||
case 'Delete':
|
||
inputText = '\x7f';
|
||
break;
|
||
case ' ':
|
||
inputText = ' ';
|
||
break;
|
||
default:
|
||
// Handle regular printable characters
|
||
if (e.key.length === 1) {
|
||
inputText = e.key;
|
||
}
|
||
else {
|
||
// Ignore other special keys
|
||
return;
|
||
}
|
||
break;
|
||
}
|
||
// Handle Ctrl combinations
|
||
if (e.ctrlKey && e.key.length === 1) {
|
||
const charCode = e.key.toLowerCase().charCodeAt(0);
|
||
if (charCode >= 97 && charCode <= 122) { // a-z
|
||
inputText = String.fromCharCode(charCode - 96); // Ctrl+A = \x01, etc.
|
||
}
|
||
}
|
||
// Send the input to the session
|
||
try {
|
||
const response = await fetch(`/api/sessions/${this.session.id}/input`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ text: inputText })
|
||
});
|
||
if (!response.ok) {
|
||
console.error('Failed to send input to session');
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('Error sending input:', error);
|
||
}
|
||
}
|
||
handleBack() {
|
||
this.dispatchEvent(new CustomEvent('back'));
|
||
}
|
||
// Mobile input methods
|
||
handleMobileInputToggle() {
|
||
this.showMobileInput = !this.showMobileInput;
|
||
if (this.showMobileInput) {
|
||
// Focus the textarea after a short delay to ensure it's rendered
|
||
setTimeout(() => {
|
||
const textarea = this.querySelector('#mobile-input-textarea');
|
||
if (textarea) {
|
||
textarea.focus();
|
||
this.adjustTextareaForKeyboard();
|
||
}
|
||
}, 100);
|
||
}
|
||
else {
|
||
// Clean up viewport listener when closing overlay
|
||
const textarea = this.querySelector('#mobile-input-textarea');
|
||
if (textarea && textarea._viewportCleanup) {
|
||
textarea._viewportCleanup();
|
||
}
|
||
}
|
||
}
|
||
adjustTextareaForKeyboard() {
|
||
// Adjust the layout when virtual keyboard appears
|
||
const textarea = this.querySelector('#mobile-input-textarea');
|
||
const controls = this.querySelector('#mobile-controls');
|
||
if (!textarea || !controls)
|
||
return;
|
||
const adjustLayout = () => {
|
||
const viewportHeight = window.visualViewport?.height || window.innerHeight;
|
||
const windowHeight = window.innerHeight;
|
||
const keyboardHeight = windowHeight - viewportHeight;
|
||
// If keyboard is visible (viewport height is significantly smaller)
|
||
if (keyboardHeight > 100) {
|
||
// Move controls above the keyboard
|
||
controls.style.transform = `translateY(-${keyboardHeight}px)`;
|
||
controls.style.transition = 'transform 0.3s ease';
|
||
// Calculate available space for textarea
|
||
const header = this.querySelector('.flex.items-center.justify-between.p-4.border-b');
|
||
const headerHeight = header?.offsetHeight || 60;
|
||
const controlsHeight = controls?.offsetHeight || 120;
|
||
const padding = 48; // Additional padding for spacing
|
||
// Available height is viewport height minus header and controls (controls are now above keyboard)
|
||
const maxTextareaHeight = viewportHeight - headerHeight - controlsHeight - padding;
|
||
const inputArea = textarea.parentElement;
|
||
if (inputArea && maxTextareaHeight > 0) {
|
||
// Set the input area to not exceed the available space
|
||
inputArea.style.height = `${maxTextareaHeight}px`;
|
||
inputArea.style.maxHeight = `${maxTextareaHeight}px`;
|
||
inputArea.style.overflow = 'hidden';
|
||
// Set textarea height within the container
|
||
const labelHeight = 40; // Height of the label above textarea
|
||
const textareaMaxHeight = Math.max(maxTextareaHeight - labelHeight, 80);
|
||
textarea.style.height = `${textareaMaxHeight}px`;
|
||
textarea.style.maxHeight = `${textareaMaxHeight}px`;
|
||
}
|
||
}
|
||
else {
|
||
// Reset position when keyboard is hidden
|
||
controls.style.transform = 'translateY(0px)';
|
||
controls.style.transition = 'transform 0.3s ease';
|
||
// Reset textarea height and constraints
|
||
const inputArea = textarea.parentElement;
|
||
if (inputArea) {
|
||
inputArea.style.height = '';
|
||
inputArea.style.maxHeight = '';
|
||
inputArea.style.overflow = '';
|
||
textarea.style.height = '';
|
||
textarea.style.maxHeight = '';
|
||
}
|
||
}
|
||
};
|
||
// Listen for viewport changes (keyboard show/hide)
|
||
if (window.visualViewport) {
|
||
window.visualViewport.addEventListener('resize', adjustLayout);
|
||
// Clean up listener when overlay is closed
|
||
const cleanup = () => {
|
||
if (window.visualViewport) {
|
||
window.visualViewport.removeEventListener('resize', adjustLayout);
|
||
}
|
||
};
|
||
// Store cleanup function for later use
|
||
textarea._viewportCleanup = cleanup;
|
||
}
|
||
// Initial adjustment
|
||
setTimeout(adjustLayout, 300);
|
||
}
|
||
handleMobileInputChange(e) {
|
||
const textarea = e.target;
|
||
this.mobileInputText = textarea.value;
|
||
}
|
||
async handleMobileInputSendOnly() {
|
||
// Get the current value from the textarea directly
|
||
const textarea = this.querySelector('#mobile-input-textarea');
|
||
const textToSend = textarea?.value?.trim() || this.mobileInputText.trim();
|
||
if (!textToSend)
|
||
return;
|
||
try {
|
||
// Send text without enter key
|
||
await this.sendInputText(textToSend);
|
||
// Clear both the reactive property and textarea
|
||
this.mobileInputText = '';
|
||
if (textarea) {
|
||
textarea.value = '';
|
||
}
|
||
// Trigger re-render to update button state
|
||
this.requestUpdate();
|
||
// Hide the input overlay after sending
|
||
this.showMobileInput = false;
|
||
}
|
||
catch (error) {
|
||
console.error('Error sending mobile input:', error);
|
||
// Don't hide the overlay if there was an error
|
||
}
|
||
}
|
||
async handleMobileInputSend() {
|
||
// Get the current value from the textarea directly
|
||
const textarea = this.querySelector('#mobile-input-textarea');
|
||
const textToSend = textarea?.value?.trim() || this.mobileInputText.trim();
|
||
if (!textToSend)
|
||
return;
|
||
try {
|
||
// Add enter key at the end to execute the command
|
||
await this.sendInputText(textToSend + '\n');
|
||
// Clear both the reactive property and textarea
|
||
this.mobileInputText = '';
|
||
if (textarea) {
|
||
textarea.value = '';
|
||
}
|
||
// Trigger re-render to update button state
|
||
this.requestUpdate();
|
||
// Hide the input overlay after sending
|
||
this.showMobileInput = false;
|
||
}
|
||
catch (error) {
|
||
console.error('Error sending mobile input:', error);
|
||
// Don't hide the overlay if there was an error
|
||
}
|
||
}
|
||
async handleSpecialKey(key) {
|
||
await this.sendInputText(key);
|
||
}
|
||
async sendInputText(text) {
|
||
if (!this.session)
|
||
return;
|
||
try {
|
||
const response = await fetch(`/api/sessions/${this.session.id}/input`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ text })
|
||
});
|
||
if (!response.ok) {
|
||
console.error('Failed to send input to session');
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('Error sending input:', error);
|
||
}
|
||
}
|
||
startSessionStatusPolling() {
|
||
if (this.sessionStatusInterval) {
|
||
clearInterval(this.sessionStatusInterval);
|
||
}
|
||
// Poll every 2 seconds
|
||
this.sessionStatusInterval = window.setInterval(() => {
|
||
this.checkSessionStatus();
|
||
}, 2000);
|
||
}
|
||
stopSessionStatusPolling() {
|
||
if (this.sessionStatusInterval) {
|
||
clearInterval(this.sessionStatusInterval);
|
||
this.sessionStatusInterval = null;
|
||
}
|
||
}
|
||
async checkSessionStatus() {
|
||
if (!this.session)
|
||
return;
|
||
try {
|
||
const response = await fetch('/api/sessions');
|
||
if (!response.ok)
|
||
return;
|
||
const sessions = await response.json();
|
||
const currentSession = sessions.find((s) => s.id === this.session.id);
|
||
if (currentSession && currentSession.status !== this.session.status) {
|
||
// Session status changed
|
||
this.session = { ...this.session, status: currentSession.status };
|
||
this.requestUpdate();
|
||
// If session ended, switch from stream to snapshot to prevent restarts
|
||
if (currentSession.status === 'exited' && this.player && this.session.status === 'running') {
|
||
console.log('Session ended, switching to snapshot view');
|
||
try {
|
||
// Dispose the streaming player
|
||
if (this.player.dispose) {
|
||
this.player.dispose();
|
||
}
|
||
this.player = null;
|
||
// Recreate with snapshot
|
||
setTimeout(() => {
|
||
this.createInteractiveTerminal();
|
||
}, 100);
|
||
}
|
||
catch (error) {
|
||
console.error('Error switching to snapshot:', error);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error('Error checking session status:', error);
|
||
}
|
||
}
|
||
render() {
|
||
if (!this.session) {
|
||
return (0, lit_1.html) `
|
||
<div class="p-4 text-vs-muted">
|
||
No session selected
|
||
</div>
|
||
`;
|
||
}
|
||
return (0, lit_1.html) `
|
||
<style>
|
||
session-view *, session-view *:focus, session-view *:focus-visible {
|
||
outline: none !important;
|
||
box-shadow: none !important;
|
||
}
|
||
</style>
|
||
<div class="h-screen flex flex-col bg-vs-bg font-mono" style="outline: none !important; box-shadow: none !important;">
|
||
<!-- Compact Header -->
|
||
<div class="flex items-center justify-between px-3 py-2 border-b border-vs-border bg-vs-bg-secondary text-sm">
|
||
<div class="flex items-center gap-3">
|
||
<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"
|
||
@click=${this.handleBack}
|
||
>
|
||
BACK
|
||
</button>
|
||
<div class="text-vs-text">
|
||
<span class="text-vs-accent">${this.session.command}</span>
|
||
<span class="text-vs-muted text-xs ml-2">(${this.session.id.substring(0, 8)}...)</span>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-3 text-xs">
|
||
<span class="text-vs-muted">
|
||
${this.session.workingDir}
|
||
</span>
|
||
<span class="${this.session.status === 'running' ? 'text-vs-user' : 'text-vs-warning'}">
|
||
${this.session.status.toUpperCase()}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Terminal Container -->
|
||
<div class="flex-1 bg-black overflow-hidden">
|
||
<div id="interactive-terminal" class="w-full h-full"></div>
|
||
</div>
|
||
|
||
<!-- Mobile Input Controls -->
|
||
${this.isMobile ? (0, lit_1.html) `
|
||
<!-- Quick Action Buttons (only when overlay is closed) -->
|
||
${!this.showMobileInput ? (0, lit_1.html) `
|
||
<div class="fixed bottom-4 left-4 right-4 z-40">
|
||
<!-- 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"
|
||
@click=${() => this.handleSpecialKey('arrow_up')}
|
||
>
|
||
↑
|
||
</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"
|
||
@click=${() => this.handleSpecialKey('arrow_down')}
|
||
>
|
||
↓
|
||
</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"
|
||
@click=${() => this.handleSpecialKey('arrow_left')}
|
||
>
|
||
←
|
||
</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"
|
||
@click=${() => this.handleSpecialKey('arrow_right')}
|
||
>
|
||
→
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 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"
|
||
@click=${() => this.handleSpecialKey('\t')}
|
||
>
|
||
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"
|
||
@click=${() => this.handleSpecialKey('enter')}
|
||
>
|
||
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"
|
||
@click=${() => this.handleSpecialKey('escape')}
|
||
>
|
||
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')}
|
||
>
|
||
^C
|
||
</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"
|
||
@click=${this.handleMobileInputToggle}
|
||
>
|
||
TYPE
|
||
</button>
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<!-- Full-Screen Input Overlay (only when opened) -->
|
||
${this.showMobileInput ? (0, lit_1.html) `
|
||
<div class="fixed inset-0 bg-vs-bg-secondary bg-opacity-95 z-50 flex flex-col" style="height: 100vh; height: 100dvh;">
|
||
<!-- Input Header -->
|
||
<div class="flex items-center justify-between p-4 border-b border-vs-border flex-shrink-0">
|
||
<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.
|
||
</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}
|
||
@keydown=${(e) => {
|
||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||
e.preventDefault();
|
||
this.handleMobileInputSend();
|
||
}
|
||
}}
|
||
style="min-height: 120px; margin-bottom: 16px;"
|
||
></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">
|
||
<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"
|
||
@click=${this.handleMobileInputSendOnly}
|
||
?disabled=${!this.mobileInputText.trim()}
|
||
>
|
||
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"
|
||
@click=${this.handleMobileInputSend}
|
||
?disabled=${!this.mobileInputText.trim()}
|
||
>
|
||
SEND + ENTER
|
||
</button>
|
||
</div>
|
||
|
||
<div class="text-vs-muted text-xs text-center">
|
||
SEND: text only • SEND + ENTER: text with enter key
|
||
</div>
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
};
|
||
exports.SessionView = SessionView;
|
||
__decorate([
|
||
(0, decorators_js_1.property)({ type: Object }),
|
||
__metadata("design:type", Object)
|
||
], SessionView.prototype, "session", void 0);
|
||
__decorate([
|
||
(0, decorators_js_1.state)(),
|
||
__metadata("design:type", Object)
|
||
], SessionView.prototype, "connected", void 0);
|
||
__decorate([
|
||
(0, decorators_js_1.state)(),
|
||
__metadata("design:type", Object)
|
||
], SessionView.prototype, "player", void 0);
|
||
__decorate([
|
||
(0, decorators_js_1.state)(),
|
||
__metadata("design:type", Object)
|
||
], SessionView.prototype, "sessionStatusInterval", void 0);
|
||
__decorate([
|
||
(0, decorators_js_1.state)(),
|
||
__metadata("design:type", Object)
|
||
], SessionView.prototype, "showMobileInput", void 0);
|
||
__decorate([
|
||
(0, decorators_js_1.state)(),
|
||
__metadata("design:type", Object)
|
||
], SessionView.prototype, "mobileInputText", void 0);
|
||
__decorate([
|
||
(0, decorators_js_1.state)(),
|
||
__metadata("design:type", Object)
|
||
], SessionView.prototype, "isMobile", void 0);
|
||
__decorate([
|
||
(0, decorators_js_1.state)(),
|
||
__metadata("design:type", Object)
|
||
], SessionView.prototype, "touchStartX", void 0);
|
||
__decorate([
|
||
(0, decorators_js_1.state)(),
|
||
__metadata("design:type", Object)
|
||
], SessionView.prototype, "touchStartY", void 0);
|
||
exports.SessionView = SessionView = __decorate([
|
||
(0, decorators_js_1.customElement)('session-view')
|
||
], SessionView);
|
||
//# sourceMappingURL=session-view.js.map
|