mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
Enhance VibeTunnel web interface with modern visual design (#177)
This commit is contained in:
parent
b452251a47
commit
fcda54a5f9
21 changed files with 632 additions and 301 deletions
|
|
@ -113,6 +113,11 @@ export class VibeTunnelApp extends LitElement {
|
|||
this.hasActiveOverlay =
|
||||
this.showFileBrowser || this.showCreateModal || this.showSSHKeyManager || this.showSettings;
|
||||
}
|
||||
|
||||
// Force re-render when sessions change or view changes to update log button position
|
||||
if (changedProperties.has('sessions') || changedProperties.has('currentView')) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
|
|
@ -1155,6 +1160,54 @@ export class VibeTunnelApp extends LitElement {
|
|||
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !('MSStream' in window);
|
||||
}
|
||||
|
||||
private getLogButtonPosition(): string {
|
||||
// Check if we're in grid view and not in split view
|
||||
const isGridView = !this.showSplitView && this.currentView === 'list';
|
||||
|
||||
if (isGridView) {
|
||||
// Calculate if we need to move the button up
|
||||
const runningSessions = this.sessions.filter((s) => s.status === 'running');
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Grid layout: auto-fill with 360px min width, 400px height, 1.25rem gap
|
||||
const gridItemHeight = 400;
|
||||
const gridGap = 20; // 1.25rem
|
||||
const containerPadding = 16; // Approximate padding
|
||||
const headerHeight = 200; // Approximate header + controls height
|
||||
|
||||
// Calculate available height for grid
|
||||
const availableHeight = viewportHeight - headerHeight;
|
||||
|
||||
// Calculate how many rows can fit
|
||||
const rowsCanFit = Math.floor(
|
||||
(availableHeight - containerPadding) / (gridItemHeight + gridGap)
|
||||
);
|
||||
|
||||
// Calculate grid columns based on viewport width
|
||||
const viewportWidth = window.innerWidth;
|
||||
const gridItemMinWidth = 360;
|
||||
const sidebarWidth = this.sidebarCollapsed
|
||||
? 0
|
||||
: this.mediaState.isMobile
|
||||
? 0
|
||||
: this.sidebarWidth;
|
||||
const availableWidth = viewportWidth - sidebarWidth - containerPadding * 2;
|
||||
const columnsCanFit = Math.floor(availableWidth / (gridItemMinWidth + gridGap));
|
||||
|
||||
// Calculate total items that can fit in viewport
|
||||
const itemsInViewport = rowsCanFit * columnsCanFit;
|
||||
|
||||
// If we have more running sessions than can fit in viewport, items will be at bottom
|
||||
if (runningSessions.length >= itemsInViewport && itemsInViewport > 0) {
|
||||
// Move button up to avoid overlapping with kill buttons
|
||||
return 'bottom-20'; // ~80px up
|
||||
}
|
||||
}
|
||||
|
||||
// Default position with equal margins
|
||||
return 'bottom-4';
|
||||
}
|
||||
|
||||
private get isInSidebarDismissMode(): boolean {
|
||||
if (!this.mediaState.isMobile || !this.shouldShowMobileOverlay) return false;
|
||||
|
||||
|
|
@ -1372,13 +1425,13 @@ export class VibeTunnelApp extends LitElement {
|
|||
@error=${this.handleError}
|
||||
></session-create-form>
|
||||
|
||||
<!-- Version and logs link in bottom right -->
|
||||
<!-- Version and logs link with smart positioning -->
|
||||
${
|
||||
this.showLogLink
|
||||
? html`
|
||||
<div class="fixed bottom-4 right-4 text-dark-text-muted text-xs font-mono z-20">
|
||||
<div class="fixed ${this.getLogButtonPosition()} right-4 text-dark-text-muted text-xs font-mono z-20 bg-dark-bg-secondary px-3 py-1.5 rounded-lg border border-dark-border shadow-sm transition-all duration-200">
|
||||
<a href="/logs" class="hover:text-dark-text transition-colors">Logs</a>
|
||||
<span class="ml-2">v${VERSION}</span>
|
||||
<span class="ml-2 opacity-75">v${VERSION}</span>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ export class AuthLogin extends LitElement {
|
|||
<div class="flex flex-col items-center gap-2 sm:gap-3 mb-4 sm:mb-8">
|
||||
<terminal-icon
|
||||
size="${this.isMobile ? '48' : '56'}"
|
||||
style="filter: drop-shadow(0 0 15px rgba(124, 230, 161, 0.4));"
|
||||
style="filter: drop-shadow(0 0 15px rgba(16, 185, 129, 0.4));"
|
||||
></terminal-icon>
|
||||
<h2 class="auth-title text-2xl sm:text-3xl mt-1 sm:mt-2">VibeTunnel</h2>
|
||||
<p class="auth-subtitle text-xs sm:text-sm">Please authenticate to continue</p>
|
||||
|
|
@ -233,7 +233,7 @@ export class AuthLogin extends LitElement {
|
|||
<div class="flex flex-col items-center mb-4 sm:mb-6">
|
||||
<div
|
||||
class="w-24 h-24 sm:w-28 sm:h-28 rounded-full mb-3 sm:mb-4 overflow-hidden"
|
||||
style="box-shadow: 0 0 25px rgba(124, 230, 161, 0.3);"
|
||||
style="box-shadow: 0 0 25px rgba(16, 185, 129, 0.3);"
|
||||
>
|
||||
${
|
||||
this.userAvatar
|
||||
|
|
@ -361,7 +361,7 @@ export class AuthLogin extends LitElement {
|
|||
<div class="ssh-key-item p-6 sm:p-8">
|
||||
<div class="flex items-center justify-between mb-3 sm:mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-accent-green"></div>
|
||||
<div class="w-2 h-2 rounded-full bg-primary"></div>
|
||||
<span class="font-mono text-xs sm:text-sm">SSH Key Management</span>
|
||||
</div>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export class AuthQuickKeys extends LitElement {
|
|||
({ key, label }) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="quick-key-btn px-3 py-1.5 bg-dark-bg-tertiary text-dark-text text-xs font-mono rounded border border-dark-border hover:bg-dark-surface hover:border-accent-green transition-all whitespace-nowrap flex-shrink-0"
|
||||
class="quick-key-btn px-3 py-1.5 bg-dark-bg-tertiary text-dark-text text-xs font-mono rounded border border-dark-border hover:bg-dark-surface hover:border-primary transition-all whitespace-nowrap flex-shrink-0"
|
||||
@click=${() => this.handleKeyPress(key)}
|
||||
>
|
||||
${label}
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ export class FileBrowser extends LitElement {
|
|||
let className = 'text-dark-text-muted';
|
||||
if (line.startsWith('+')) className = 'text-status-success bg-green-900/20';
|
||||
else if (line.startsWith('-')) className = 'text-status-error bg-red-900/20';
|
||||
else if (line.startsWith('@@')) className = 'text-accent-blue font-semibold';
|
||||
else if (line.startsWith('@@')) className = 'text-status-info font-semibold';
|
||||
|
||||
return html`<div class="whitespace-pre ${className}">${line}</div>`;
|
||||
})}
|
||||
|
|
@ -550,7 +550,7 @@ export class FileBrowser extends LitElement {
|
|||
@input=${this.handlePathInput}
|
||||
@keydown=${this.handlePathKeyDown}
|
||||
@blur=${this.handlePathBlur}
|
||||
class="bg-dark-bg border border-dark-border rounded px-2 py-1 text-blue-400 text-xs sm:text-sm font-mono w-full min-w-0 focus:outline-none focus:border-accent-green"
|
||||
class="bg-dark-bg border border-dark-border rounded px-2 py-1 text-status-info text-xs sm:text-sm font-mono w-full min-w-0 focus:outline-none focus:border-primary"
|
||||
placeholder="Enter path and press Enter"
|
||||
/>
|
||||
`
|
||||
|
|
@ -598,7 +598,7 @@ export class FileBrowser extends LitElement {
|
|||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn-secondary text-xs px-2 py-1 font-mono ${
|
||||
this.gitFilter === 'changed' ? 'bg-accent-green text-dark-bg' : ''
|
||||
this.gitFilter === 'changed' ? 'bg-primary text-black' : ''
|
||||
}"
|
||||
@click=${this.toggleGitFilter}
|
||||
title="Show only Git changes"
|
||||
|
|
@ -607,7 +607,7 @@ export class FileBrowser extends LitElement {
|
|||
</button>
|
||||
<button
|
||||
class="btn-secondary text-xs px-2 py-1 font-mono ${
|
||||
this.showHidden ? 'bg-accent-green text-dark-bg' : ''
|
||||
this.showHidden ? 'bg-primary text-black' : ''
|
||||
}"
|
||||
@click=${this.toggleHidden}
|
||||
title="Show hidden files"
|
||||
|
|
@ -657,7 +657,7 @@ export class FileBrowser extends LitElement {
|
|||
class="p-3 hover:bg-dark-bg-lighter cursor-pointer transition-colors flex items-center gap-2
|
||||
${
|
||||
this.selectedFile?.path === file.path
|
||||
? 'bg-dark-bg-lighter border-l-2 border-accent-green'
|
||||
? 'bg-dark-bg-lighter border-l-2 border-primary'
|
||||
: ''
|
||||
}"
|
||||
@click=${() => this.handleFileClick(file)}
|
||||
|
|
@ -684,7 +684,7 @@ export class FileBrowser extends LitElement {
|
|||
</span>
|
||||
<span
|
||||
class="flex-1 text-sm whitespace-nowrap ${
|
||||
file.type === 'directory' ? 'text-accent-blue' : 'text-dark-text'
|
||||
file.type === 'directory' ? 'text-status-info' : 'text-dark-text'
|
||||
}"
|
||||
title="${file.name}${file.isSymlink ? ' (symlink)' : ''}"
|
||||
>${file.name}</span
|
||||
|
|
@ -805,7 +805,7 @@ export class FileBrowser extends LitElement {
|
|||
? html`
|
||||
<button
|
||||
class="btn-secondary text-xs px-2 py-1 font-mono ${
|
||||
this.showDiff ? 'bg-accent-green text-dark-bg' : ''
|
||||
this.showDiff ? 'bg-primary text-black' : ''
|
||||
} ${
|
||||
this.isMobile &&
|
||||
this.selectedFile.type === 'file' &&
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// @vitest-environment happy-dom
|
||||
/**
|
||||
* Unit tests for FilePicker component
|
||||
*/
|
||||
|
|
@ -63,7 +64,7 @@ describe('FilePicker Component', () => {
|
|||
const progressText = element.querySelector('span');
|
||||
expect(progressText?.textContent).toContain('Uploading...');
|
||||
|
||||
const progressBar = element.querySelector('.bg-blue-500');
|
||||
const progressBar = element.querySelector('.bg-gradient-to-r');
|
||||
expect(progressBar).toBeTruthy();
|
||||
});
|
||||
|
||||
|
|
@ -115,7 +116,7 @@ describe('FilePicker Component', () => {
|
|||
const cancelEventSpy = vi.fn();
|
||||
element.addEventListener('file-cancel', cancelEventSpy);
|
||||
|
||||
const modal = element.querySelector('.bg-white');
|
||||
const modal = element.querySelector('.bg-dark-bg-elevated');
|
||||
expect(modal).toBeTruthy();
|
||||
|
||||
modal?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export class FilePicker extends LitElement {
|
|||
|
||||
@property({ type: Boolean }) visible = false;
|
||||
@property({ type: Boolean }) showPathOption = true; // Whether to show "Send path to terminal" option
|
||||
@property({ type: Boolean }) directSelect = false; // Skip dialog and open file picker directly
|
||||
@state() private uploading = false;
|
||||
@state() private uploadProgress = 0;
|
||||
|
||||
|
|
@ -45,6 +46,20 @@ export class FilePicker extends LitElement {
|
|||
this.createFileInput();
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
// If directSelect is enabled and visible becomes true, immediately open file picker
|
||||
if (changedProperties.has('visible') && this.visible && this.directSelect) {
|
||||
// Small delay to ensure the component is ready
|
||||
setTimeout(() => {
|
||||
this.handleFileClick();
|
||||
// Reset visible state since we're not showing the dialog
|
||||
this.visible = false;
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.fileInput) {
|
||||
|
|
@ -94,6 +109,13 @@ export class FilePicker extends LitElement {
|
|||
return this.uploadFileToServer(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to directly open the file picker without showing dialog
|
||||
*/
|
||||
openFilePicker(): void {
|
||||
this.handleFileClick();
|
||||
}
|
||||
|
||||
private async uploadFileToServer(file: File): Promise<void> {
|
||||
this.uploading = true;
|
||||
this.uploadProgress = 0;
|
||||
|
|
@ -187,52 +209,53 @@ export class FilePicker extends LitElement {
|
|||
}
|
||||
|
||||
render() {
|
||||
// Always render a container so the file input is available
|
||||
if (!this.visible) {
|
||||
return html``;
|
||||
return html`<div style="display: none;"></div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" @click=${this.handleCancel}>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 m-4 max-w-sm w-full" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-80 backdrop-blur-sm flex items-center justify-center z-50 animate-fade-in" @click=${this.handleCancel}>
|
||||
<div class="bg-dark-bg-elevated border border-dark-border rounded-xl shadow-2xl p-8 m-4 max-w-sm w-full animate-scale-in" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<h3 class="text-xl font-bold text-dark-text mb-6">
|
||||
Select File
|
||||
</h3>
|
||||
|
||||
${
|
||||
this.uploading
|
||||
? html`
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Uploading...</span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">${Math.round(this.uploadProgress)}%</span>
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm text-dark-text-muted font-mono">Uploading...</span>
|
||||
<span class="text-sm text-accent-primary font-mono font-medium">${Math.round(this.uploadProgress)}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div class="w-full bg-dark-bg-secondary rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
class="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
||||
class="bg-gradient-to-r from-accent-primary to-accent-primary-light h-2 rounded-full transition-all duration-300 shadow-glow-primary-sm"
|
||||
style="width: ${this.uploadProgress}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-4">
|
||||
<button
|
||||
@click=${this.handleFileClick}
|
||||
class="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-3 px-4 rounded-lg flex items-center justify-center space-x-2 transition-colors"
|
||||
class="w-full bg-accent-primary text-dark-bg font-medium py-4 px-6 rounded-lg flex items-center justify-center gap-3 transition-all duration-200 hover:bg-accent-primary-light hover:shadow-glow-primary active:scale-95"
|
||||
>
|
||||
<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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-5L9 2H4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span>Choose File</span>
|
||||
<span class="font-mono">Choose File</span>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
|
||||
<div class="mt-6 pt-6 border-t border-dark-border">
|
||||
<button
|
||||
@click=${this.handleCancel}
|
||||
class="w-full bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
class="w-full bg-dark-bg-secondary border border-dark-border text-dark-text font-mono py-3 px-6 rounded-lg transition-all duration-200 hover:bg-dark-surface-hover hover:border-dark-border-light active:scale-95"
|
||||
?disabled=${this.uploading}
|
||||
>
|
||||
Cancel
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ describe('SessionCard', () => {
|
|||
await element.updateComplete;
|
||||
|
||||
const card = element.querySelector('.card');
|
||||
expect(card?.classList.contains('shadow-glow-green-sm')).toBe(true);
|
||||
expect(card?.classList.contains('shadow-glow-sm')).toBe(true);
|
||||
});
|
||||
|
||||
it('should apply opacity when killing', async () => {
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ export class SessionCard extends LitElement {
|
|||
this.killing ? 'opacity-60' : ''
|
||||
} ${
|
||||
this.isActive && this.session.status === 'running'
|
||||
? 'shadow-[0_0_0_2px_#00ff88] shadow-glow-green-sm'
|
||||
? 'ring-2 ring-primary shadow-glow-sm'
|
||||
: ''
|
||||
}"
|
||||
style="view-transition-name: session-${this.session.id}; --session-id: session-${
|
||||
|
|
@ -238,7 +238,7 @@ export class SessionCard extends LitElement {
|
|||
>
|
||||
<!-- Compact Header -->
|
||||
<div
|
||||
class="flex justify-between items-center px-3 py-2 border-b border-dark-border bg-dark-bg-secondary"
|
||||
class="flex justify-between items-center px-3 py-2 border-b border-dark-border bg-gradient-to-r from-dark-bg-secondary to-dark-bg-tertiary"
|
||||
>
|
||||
<div class="text-xs font-mono pr-2 flex-1 min-w-0 text-accent-green">
|
||||
<div class="truncate" title="${this.session.name || this.session.command.join(' ')}">
|
||||
|
|
@ -249,12 +249,10 @@ export class SessionCard extends LitElement {
|
|||
this.session.status === 'running' || this.session.status === 'exited'
|
||||
? html`
|
||||
<button
|
||||
class="btn-ghost ${
|
||||
this.session.status === 'running' ? 'text-status-error' : 'text-status-warning'
|
||||
} disabled:opacity-50 flex-shrink-0 p-1 rounded-full hover:bg-opacity-20 transition-all ${
|
||||
class="p-1 rounded-full transition-all duration-200 disabled:opacity-50 flex-shrink-0 ${
|
||||
this.session.status === 'running'
|
||||
? 'hover:bg-status-error'
|
||||
: 'hover:bg-status-warning'
|
||||
? 'text-status-error hover:bg-status-error hover:bg-opacity-20'
|
||||
: 'text-status-warning hover:bg-status-warning hover:bg-opacity-20'
|
||||
}"
|
||||
@click=${this.handleKillClick}
|
||||
?disabled=${this.killing}
|
||||
|
|
@ -292,9 +290,10 @@ export class SessionCard extends LitElement {
|
|||
|
||||
<!-- Terminal display (main content) -->
|
||||
<div
|
||||
class="session-preview bg-black overflow-hidden flex-1 ${
|
||||
class="session-preview bg-black overflow-hidden flex-1 relative ${
|
||||
this.session.status === 'exited' ? 'session-exited' : ''
|
||||
}"
|
||||
style="background: linear-gradient(to bottom, #0a0a0a, #080808); box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);"
|
||||
>
|
||||
${
|
||||
this.killing
|
||||
|
|
@ -319,18 +318,20 @@ export class SessionCard extends LitElement {
|
|||
|
||||
<!-- Compact Footer -->
|
||||
<div
|
||||
class="px-3 py-2 text-dark-text-muted text-xs border-t border-dark-border bg-dark-bg-secondary"
|
||||
class="px-3 py-2 text-dark-text-muted text-xs border-t border-dark-border bg-gradient-to-r from-dark-bg-tertiary to-dark-bg-secondary"
|
||||
>
|
||||
<div class="flex justify-between items-center min-w-0">
|
||||
<span
|
||||
class="${this.getStatusColor()} text-xs flex items-center gap-1 flex-shrink-0"
|
||||
class="${this.getActivityStatusColor()} text-xs flex items-center gap-1 flex-shrink-0"
|
||||
data-status="${this.session.status}"
|
||||
data-killing="${this.killing}"
|
||||
>
|
||||
<div class="w-2 h-2 rounded-full ${this.getStatusDotColor()}"></div>
|
||||
${this.getStatusText()}
|
||||
${this.getActivityStatusText()}
|
||||
${
|
||||
this.session.status === 'running' && this.isActive
|
||||
this.session.status === 'running' &&
|
||||
this.isActive &&
|
||||
!this.session.activityStatus?.specificStatus
|
||||
? html`<span class="text-accent-green animate-pulse ml-1">●</span>`
|
||||
: ''
|
||||
}
|
||||
|
|
@ -364,6 +365,19 @@ export class SessionCard extends LitElement {
|
|||
return this.session.status;
|
||||
}
|
||||
|
||||
private getActivityStatusText(): string {
|
||||
if (this.killing) {
|
||||
return 'killing...';
|
||||
}
|
||||
if (this.session.active === false) {
|
||||
return 'waiting';
|
||||
}
|
||||
if (this.session.status === 'running' && this.session.activityStatus?.specificStatus) {
|
||||
return this.session.activityStatus.specificStatus.status;
|
||||
}
|
||||
return this.session.status;
|
||||
}
|
||||
|
||||
private getStatusColor(): string {
|
||||
if (this.killing) {
|
||||
return 'text-status-error';
|
||||
|
|
@ -374,6 +388,19 @@ export class SessionCard extends LitElement {
|
|||
return this.session.status === 'running' ? 'text-status-success' : 'text-status-warning';
|
||||
}
|
||||
|
||||
private getActivityStatusColor(): string {
|
||||
if (this.killing) {
|
||||
return 'text-status-error';
|
||||
}
|
||||
if (this.session.active === false) {
|
||||
return 'text-dark-text-muted';
|
||||
}
|
||||
if (this.session.status === 'running' && this.session.activityStatus?.specificStatus) {
|
||||
return 'text-status-warning';
|
||||
}
|
||||
return this.session.status === 'running' ? 'text-status-success' : 'text-status-warning';
|
||||
}
|
||||
|
||||
private getStatusDotColor(): string {
|
||||
if (this.killing) {
|
||||
return 'bg-status-error animate-pulse';
|
||||
|
|
@ -381,6 +408,15 @@ export class SessionCard extends LitElement {
|
|||
if (this.session.active === false) {
|
||||
return 'bg-dark-text-muted';
|
||||
}
|
||||
return this.session.status === 'running' ? 'bg-status-success' : 'bg-status-warning';
|
||||
if (this.session.status === 'running') {
|
||||
if (this.session.activityStatus?.specificStatus) {
|
||||
return 'bg-status-warning animate-pulse'; // Claude active - amber with pulse
|
||||
} else if (this.session.activityStatus?.isActive || this.isActive) {
|
||||
return 'bg-status-success'; // Generic active - solid green
|
||||
} else {
|
||||
return 'bg-status-success ring-1 ring-status-success ring-opacity-50'; // Idle - green with ring
|
||||
}
|
||||
}
|
||||
return 'bg-status-warning';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -380,16 +380,16 @@ export class SessionCreateForm extends LitElement {
|
|||
class="modal-content font-mono text-sm w-full max-w-[calc(100vw-1rem)] sm:max-w-md lg:max-w-[576px] mx-2 sm:mx-4"
|
||||
style="view-transition-name: create-session-modal"
|
||||
>
|
||||
<div class="p-4 pb-4 mb-3 border-b border-dark-border relative">
|
||||
<h2 class="text-accent-green text-lg font-bold">New Session</h2>
|
||||
<div class="p-6 pb-4 mb-3 border-b border-dark-border relative bg-gradient-to-r from-dark-bg-secondary to-dark-bg-tertiary">
|
||||
<h2 class="text-primary text-xl font-bold">New Session</h2>
|
||||
<button
|
||||
class="absolute top-4 right-4 text-dark-text-muted hover:text-dark-text transition-colors p-1"
|
||||
class="absolute top-6 right-6 text-dark-text-muted hover:text-dark-text transition-all duration-200 p-2 hover:bg-dark-bg-tertiary rounded-lg"
|
||||
@click=${this.handleCancel}
|
||||
title="Close"
|
||||
title="Close (Esc)"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -405,10 +405,10 @@ export class SessionCreateForm extends LitElement {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-3 sm:p-3 lg:p-4">
|
||||
<div class="p-6">
|
||||
<!-- Session Name -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Session Name (Optional):</label>
|
||||
<div class="mb-5">
|
||||
<label class="form-label text-dark-text-muted">Session Name (Optional):</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input-field"
|
||||
|
|
@ -420,8 +420,8 @@ export class SessionCreateForm extends LitElement {
|
|||
</div>
|
||||
|
||||
<!-- Command -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Command:</label>
|
||||
<div class="mb-5">
|
||||
<label class="form-label text-dark-text-muted">Command:</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input-field"
|
||||
|
|
@ -433,9 +433,9 @@ export class SessionCreateForm extends LitElement {
|
|||
</div>
|
||||
|
||||
<!-- Working Directory -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Working Directory:</label>
|
||||
<div class="flex gap-4">
|
||||
<div class="mb-5">
|
||||
<label class="form-label text-dark-text-muted">Working Directory:</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="input-field"
|
||||
|
|
@ -445,27 +445,32 @@ export class SessionCreateForm extends LitElement {
|
|||
?disabled=${this.disabled || this.isCreating}
|
||||
/>
|
||||
<button
|
||||
class="btn-secondary font-mono px-4"
|
||||
class="bg-dark-bg-elevated border border-dark-border rounded-lg p-3 font-mono text-dark-text-muted transition-all duration-200 hover:text-primary hover:bg-dark-surface-hover hover:border-primary hover:shadow-sm flex-shrink-0"
|
||||
@click=${this.handleBrowse}
|
||||
?disabled=${this.disabled || this.isCreating}
|
||||
title="Browse directories"
|
||||
>
|
||||
📁
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M1.75 1h5.5c.966 0 1.75.784 1.75 1.75v1h4c.966 0 1.75.784 1.75 1.75v7.75A1.75 1.75 0 0113 15H3a1.75 1.75 0 01-1.75-1.75V2.75C1.25 1.784 1.784 1 1.75 1zM2.75 2.5v10.75c0 .138.112.25.25.25h10a.25.25 0 00.25-.25V5.5a.25.25 0 00-.25-.25H8.75v-2.5a.25.25 0 00-.25-.25h-5.5a.25.25 0 00-.25.25z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spawn Window Toggle -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="mb-5 flex items-center justify-between bg-dark-bg-elevated border border-dark-border rounded-lg p-4">
|
||||
<div class="flex-1 pr-4">
|
||||
<span class="text-dark-text text-sm">Spawn window</span>
|
||||
<p class="text-xs text-dark-text-muted mt-1">Opens native terminal window</p>
|
||||
<span class="text-dark-text text-sm font-medium">Spawn window</span>
|
||||
<p class="text-xs text-dark-text-muted mt-0.5">Opens native terminal window</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked="${this.spawnWindow}"
|
||||
@click=${this.handleSpawnWindowChange}
|
||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-accent-green focus:ring-offset-2 focus:ring-offset-dark-bg ${
|
||||
this.spawnWindow ? 'bg-accent-green' : 'bg-dark-border'
|
||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-dark-bg ${
|
||||
this.spawnWindow ? 'bg-primary' : 'bg-dark-border'
|
||||
}"
|
||||
?disabled=${this.disabled || this.isCreating}
|
||||
>
|
||||
|
|
@ -478,10 +483,10 @@ export class SessionCreateForm extends LitElement {
|
|||
</div>
|
||||
|
||||
<!-- Terminal Title Mode -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="mb-6 flex items-center justify-between bg-dark-bg-elevated border border-dark-border rounded-lg p-4">
|
||||
<div class="flex-1 pr-4">
|
||||
<span class="text-dark-text text-sm">Terminal Title Mode</span>
|
||||
<p class="text-xs text-dark-text-muted mt-1 opacity-50">
|
||||
<span class="text-dark-text text-sm font-medium">Terminal Title Mode</span>
|
||||
<p class="text-xs text-dark-text-muted mt-0.5">
|
||||
${this.getTitleModeDescription()}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -489,14 +494,14 @@ export class SessionCreateForm extends LitElement {
|
|||
<select
|
||||
.value=${this.titleMode}
|
||||
@change=${this.handleTitleModeChange}
|
||||
class="bg-[#1a1a1a] border border-dark-border rounded-lg px-3 py-2 pr-8 text-dark-text text-sm transition-all duration-200 hover:border-accent-green-darker focus:border-accent-green focus:outline-none appearance-none cursor-pointer"
|
||||
class="bg-dark-bg-secondary border border-dark-border rounded-lg px-3 py-2 pr-8 text-dark-text text-sm transition-all duration-200 hover:border-primary-hover focus:border-primary focus:outline-none appearance-none cursor-pointer"
|
||||
style="min-width: 140px"
|
||||
?disabled=${this.disabled || this.isCreating}
|
||||
>
|
||||
<option value="${TitleMode.NONE}" class="bg-[#1a1a1a] text-dark-text" ?selected=${this.titleMode === TitleMode.NONE}>None</option>
|
||||
<option value="${TitleMode.FILTER}" class="bg-[#1a1a1a] text-dark-text" ?selected=${this.titleMode === TitleMode.FILTER}>Filter</option>
|
||||
<option value="${TitleMode.STATIC}" class="bg-[#1a1a1a] text-dark-text" ?selected=${this.titleMode === TitleMode.STATIC}>Static</option>
|
||||
<option value="${TitleMode.DYNAMIC}" class="bg-[#1a1a1a] text-dark-text" ?selected=${this.titleMode === TitleMode.DYNAMIC}>Dynamic</option>
|
||||
<option value="${TitleMode.NONE}" class="bg-dark-bg-secondary text-dark-text" ?selected=${this.titleMode === TitleMode.NONE}>None</option>
|
||||
<option value="${TitleMode.FILTER}" class="bg-dark-bg-secondary text-dark-text" ?selected=${this.titleMode === TitleMode.FILTER}>Filter</option>
|
||||
<option value="${TitleMode.STATIC}" class="bg-dark-bg-secondary text-dark-text" ?selected=${this.titleMode === TitleMode.STATIC}>Static</option>
|
||||
<option value="${TitleMode.DYNAMIC}" class="bg-dark-bg-secondary text-dark-text" ?selected=${this.titleMode === TitleMode.DYNAMIC}>Dynamic</option>
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-dark-text-muted">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -507,8 +512,8 @@ export class SessionCreateForm extends LitElement {
|
|||
</div>
|
||||
|
||||
<!-- Quick Start Section -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-dark-text-muted uppercase text-xs tracking-wider"
|
||||
<div class="mb-6">
|
||||
<label class="form-label text-dark-text-muted uppercase text-xs tracking-wider mb-3"
|
||||
>Quick Start</label
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-3 mt-2">
|
||||
|
|
@ -518,8 +523,8 @@ export class SessionCreateForm extends LitElement {
|
|||
@click=${() => this.handleQuickStart(command)}
|
||||
class="${
|
||||
this.command === command
|
||||
? 'px-4 py-3 rounded border text-left transition-all bg-accent-green bg-opacity-20 border-accent-green text-accent-green'
|
||||
: 'px-4 py-3 rounded border text-left transition-all bg-dark-border bg-opacity-10 border-dark-border text-dark-text hover:bg-opacity-20 hover:border-dark-text-secondary'
|
||||
? 'px-4 py-3 rounded-lg border text-left transition-all bg-primary bg-opacity-10 border-primary text-primary hover:bg-opacity-20 font-medium'
|
||||
: 'px-4 py-3 rounded-lg border text-left transition-all bg-dark-bg-elevated border-dark-border text-dark-text hover:bg-dark-surface-hover hover:border-primary hover:text-primary'
|
||||
}"
|
||||
?disabled=${this.disabled || this.isCreating}
|
||||
>
|
||||
|
|
@ -532,16 +537,16 @@ export class SessionCreateForm extends LitElement {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-4">
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button
|
||||
class="btn-ghost font-mono flex-1 py-3"
|
||||
class="flex-1 bg-dark-bg-elevated border border-dark-border text-dark-text px-6 py-3 rounded-lg font-mono text-sm transition-all duration-200 hover:bg-dark-surface-hover hover:border-dark-border-light"
|
||||
@click=${this.handleCancel}
|
||||
?disabled=${this.isCreating}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="btn-primary font-mono flex-1 py-3 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="flex-1 bg-primary text-black px-6 py-3 rounded-lg font-mono text-sm font-medium transition-all duration-200 hover:bg-primary-hover hover:shadow-glow disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
@click=${this.handleCreate}
|
||||
?disabled=${
|
||||
this.disabled ||
|
||||
|
|
|
|||
|
|
@ -148,14 +148,19 @@ export class SessionList extends LitElement {
|
|||
}
|
||||
|
||||
render() {
|
||||
const filteredSessions = this.hideExited
|
||||
? this.sessions.filter((session) => session.status !== 'exited')
|
||||
: this.sessions;
|
||||
// Group sessions by status
|
||||
const runningSessions = this.sessions.filter((session) => session.status === 'running');
|
||||
const exitedSessions = this.sessions.filter((session) => session.status === 'exited');
|
||||
|
||||
const hasRunningSessions = runningSessions.length > 0;
|
||||
const hasExitedSessions = exitedSessions.length > 0;
|
||||
const showExitedSection = !this.hideExited && hasExitedSessions;
|
||||
|
||||
return html`
|
||||
<div class="font-mono text-sm p-4 bg-black" data-testid="session-list-container">
|
||||
<div class="font-mono text-sm" data-testid="session-list-container">
|
||||
<div class="p-4 pt-5">
|
||||
${
|
||||
filteredSessions.length === 0
|
||||
!hasRunningSessions && (!hasExitedSessions || this.hideExited)
|
||||
? html`
|
||||
<div class="text-dark-text-muted text-center py-8">
|
||||
${
|
||||
|
|
@ -229,11 +234,19 @@ export class SessionList extends LitElement {
|
|||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="${this.compactMode ? 'space-y-2' : 'session-flex-responsive'}">
|
||||
${repeat(
|
||||
filteredSessions,
|
||||
(session) => session.id,
|
||||
(session) => html`
|
||||
<!-- Active Sessions -->
|
||||
${
|
||||
hasRunningSessions
|
||||
? html`
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xs font-semibold text-dark-text-muted uppercase tracking-wider mb-4">
|
||||
Active <span class="text-dark-text-dim">(${runningSessions.length})</span>
|
||||
</h3>
|
||||
<div class="${this.compactMode ? 'space-y-2' : 'session-flex-responsive'} relative">
|
||||
${repeat(
|
||||
runningSessions,
|
||||
(session) => session.id,
|
||||
(session) => html`
|
||||
${
|
||||
this.compactMode
|
||||
? html`
|
||||
|
|
@ -409,10 +422,144 @@ export class SessionList extends LitElement {
|
|||
`
|
||||
}
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
|
||||
<!-- Idle/Exited Sessions -->
|
||||
${
|
||||
showExitedSection
|
||||
? html`
|
||||
<div>
|
||||
<h3 class="text-xs font-semibold text-dark-text-muted uppercase tracking-wider mb-4">
|
||||
Idle <span class="text-dark-text-dim">(${exitedSessions.length})</span>
|
||||
</h3>
|
||||
<div class="${this.compactMode ? 'space-y-2' : 'session-flex-responsive'} relative">
|
||||
${repeat(
|
||||
exitedSessions,
|
||||
(session) => session.id,
|
||||
(session) => html`
|
||||
${
|
||||
this.compactMode
|
||||
? html`
|
||||
<!-- Enhanced compact list item for sidebar -->
|
||||
<div
|
||||
class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer transition-all duration-200 animate-fade-in ${
|
||||
session.id === this.selectedSessionId
|
||||
? 'bg-dark-bg-elevated border border-accent-primary shadow-card-hover'
|
||||
: 'bg-dark-bg-secondary border border-dark-border hover:bg-dark-bg-tertiary hover:border-dark-border-light hover:shadow-card opacity-75'
|
||||
}"
|
||||
@click=${() =>
|
||||
this.handleSessionSelect({ detail: session } as CustomEvent)}
|
||||
>
|
||||
<!-- Status indicator -->
|
||||
<div class="relative flex-shrink-0">
|
||||
<div class="w-2.5 h-2.5 rounded-full bg-status-warning"></div>
|
||||
</div>
|
||||
|
||||
<!-- Elegant divider line -->
|
||||
<div class="w-px h-8 bg-gradient-to-b from-transparent via-dark-border to-transparent"></div>
|
||||
|
||||
<!-- Session content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div
|
||||
class="text-sm font-mono truncate ${
|
||||
session.id === this.selectedSessionId
|
||||
? 'text-accent-primary font-medium'
|
||||
: 'text-dark-text-muted group-hover:text-dark-text transition-colors'
|
||||
}"
|
||||
title="${
|
||||
session.name ||
|
||||
(Array.isArray(session.command)
|
||||
? session.command.join(' ')
|
||||
: session.command)
|
||||
}"
|
||||
>
|
||||
${
|
||||
session.name ||
|
||||
(Array.isArray(session.command)
|
||||
? session.command.join(' ')
|
||||
: session.command)
|
||||
}
|
||||
</div>
|
||||
<div class="text-xs text-dark-text-dim truncate">
|
||||
${formatPathForDisplay(session.workingDir)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side: duration and close button -->
|
||||
<div class="relative flex items-center flex-shrink-0">
|
||||
<!-- Session duration -->
|
||||
<div class="text-xs text-dark-text-dim font-mono">
|
||||
${session.startedAt ? formatSessionDuration(session.startedAt) : ''}
|
||||
</div>
|
||||
|
||||
<!-- Clean up button -->
|
||||
<button
|
||||
class="btn-ghost text-dark-text-muted p-1.5 rounded-md transition-all ml-2 hover:text-status-warning hover:bg-dark-bg-elevated hover:shadow-sm"
|
||||
@click=${async (e: Event) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/sessions/${session.id}/cleanup`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: this.authClient.getAuthHeader(),
|
||||
}
|
||||
);
|
||||
if (response.ok) {
|
||||
this.handleSessionKilled({
|
||||
detail: { sessionId: session.id },
|
||||
} as CustomEvent);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to clean up session', error);
|
||||
}
|
||||
}}
|
||||
title="Clean up session"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
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>
|
||||
`
|
||||
: html`
|
||||
<!-- Full session card for main view -->
|
||||
<session-card
|
||||
.session=${session}
|
||||
.authClient=${this.authClient}
|
||||
@session-select=${this.handleSessionSelect}
|
||||
@session-killed=${this.handleSessionKilled}
|
||||
@session-kill-error=${this.handleSessionKillError}
|
||||
>
|
||||
</session-card>
|
||||
`
|
||||
}
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
|
||||
${this.renderExitedControls()}
|
||||
</div>
|
||||
|
|
@ -427,45 +574,25 @@ export class SessionList extends LitElement {
|
|||
if (exitedSessions.length === 0 && runningSessions.length === 0) return '';
|
||||
|
||||
return html`
|
||||
<div class="flex flex-col sm:flex-row sm:flex-wrap gap-2 mt-8 pb-4 px-4 w-full">
|
||||
<!-- First group: Show/Hide Exited and Clean Exited (when visible) -->
|
||||
<div class="sticky bottom-0 border-t border-dark-border bg-dark-bg-secondary p-3 flex flex-wrap gap-2 shadow-lg">
|
||||
<!-- Control buttons with consistent styling -->
|
||||
${
|
||||
exitedSessions.length > 0
|
||||
? html`
|
||||
<div class="flex flex-col gap-2 w-full sm:w-auto">
|
||||
<!-- Show/Hide Exited button -->
|
||||
<button
|
||||
class="font-mono text-xs sm:text-sm px-3 sm:px-6 py-2 rounded-lg border transition-all duration-200 flex-1 sm:flex-none sm:w-auto sm:min-w-[180px] ${
|
||||
class="font-mono text-xs px-4 py-2 rounded-lg border transition-all duration-200 ${
|
||||
this.hideExited
|
||||
? 'border-dark-border bg-dark-bg-secondary text-dark-text-muted hover:bg-dark-bg-tertiary hover:text-dark-text'
|
||||
: 'border-dark-border bg-dark-bg-tertiary text-dark-text hover:bg-dark-bg-secondary'
|
||||
? 'border-dark-border bg-dark-bg-elevated text-dark-text-muted hover:bg-dark-surface-hover hover:text-accent-primary hover:border-accent-primary hover:shadow-sm'
|
||||
: 'border-accent-primary bg-accent-primary bg-opacity-10 text-accent-primary hover:bg-opacity-20 hover:shadow-glow-primary-sm'
|
||||
}"
|
||||
@click=${() =>
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('hide-exited-change', { detail: !this.hideExited })
|
||||
)}
|
||||
>
|
||||
<div class="flex items-center justify-center gap-2 sm:gap-3">
|
||||
<span class="hidden sm:inline"
|
||||
>${this.hideExited ? 'Show' : 'Hide'} Exited (${exitedSessions.length})</span
|
||||
>
|
||||
<span class="sm:hidden"
|
||||
>${this.hideExited ? 'Show' : 'Hide'} (${exitedSessions.length})</span
|
||||
>
|
||||
<div
|
||||
class="w-8 h-4 rounded-full transition-colors duration-200 ${
|
||||
this.hideExited ? 'bg-dark-surface' : 'bg-dark-bg'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="w-3 h-3 rounded-full transition-transform duration-200 mt-0.5 ${
|
||||
this.hideExited
|
||||
? 'translate-x-0.5 bg-dark-text-muted'
|
||||
: 'translate-x-4 bg-accent-green'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
${this.hideExited ? 'Show' : 'Hide'} Exited
|
||||
<span class="text-dark-text-dim">(${exitedSessions.length})</span>
|
||||
</button>
|
||||
|
||||
<!-- Clean Exited button (only when Show Exited is active) -->
|
||||
|
|
@ -473,23 +600,15 @@ export class SessionList extends LitElement {
|
|||
!this.hideExited
|
||||
? html`
|
||||
<button
|
||||
class="font-mono text-xs sm:text-sm px-3 sm:px-6 py-2 rounded-lg border transition-all duration-200 flex-1 sm:flex-none sm:w-auto sm:min-w-[120px] border-dark-border bg-dark-bg-secondary text-status-warning hover:bg-dark-bg-tertiary hover:border-status-warning"
|
||||
class="font-mono text-xs px-4 py-2 rounded-lg border transition-all duration-200 border-status-warning bg-status-warning bg-opacity-10 text-status-warning hover:bg-opacity-20 hover:shadow-glow-warning-sm disabled:opacity-50"
|
||||
@click=${this.handleCleanupExited}
|
||||
?disabled=${this.cleaningExited}
|
||||
>
|
||||
<span class="hidden sm:inline"
|
||||
>${
|
||||
this.cleaningExited
|
||||
? 'Cleaning...'
|
||||
: `Clean Exited (${exitedSessions.length})`
|
||||
}</span
|
||||
>
|
||||
<span class="sm:hidden">${this.cleaningExited ? 'Cleaning...' : 'Clean'}</span>
|
||||
${this.cleaningExited ? 'Cleaning...' : 'Clean Exited'}
|
||||
</button>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
|
|
@ -499,10 +618,10 @@ export class SessionList extends LitElement {
|
|||
runningSessions.length > 0
|
||||
? html`
|
||||
<button
|
||||
class="font-mono text-xs sm:text-sm px-3 sm:px-6 py-2 rounded-lg border transition-all duration-200 w-full sm:w-auto sm:min-w-[120px] border-status-error bg-dark-bg-secondary text-status-error hover:bg-dark-bg-tertiary hover:border-status-error"
|
||||
class="font-mono text-xs px-4 py-2 rounded-md border transition-all duration-200 border-status-error bg-status-error bg-opacity-10 text-status-error hover:bg-opacity-20 ml-auto"
|
||||
@click=${() => this.dispatchEvent(new CustomEvent('kill-all-sessions'))}
|
||||
>
|
||||
Kill All (${runningSessions.length})
|
||||
Kill All <span class="text-dark-text-dim">(${runningSessions.length})</span>
|
||||
</button>
|
||||
`
|
||||
: ''
|
||||
|
|
|
|||
|
|
@ -761,7 +761,16 @@ export class SessionView extends LitElement {
|
|||
}
|
||||
|
||||
private handleOpenFilePicker() {
|
||||
this.showImagePicker = true;
|
||||
if (!this.isMobile) {
|
||||
// On desktop, directly open the file picker without showing the dialog
|
||||
const filePicker = this.querySelector('file-picker') as FilePicker | null;
|
||||
if (filePicker && typeof filePicker.openFilePicker === 'function') {
|
||||
filePicker.openFilePicker();
|
||||
}
|
||||
} else {
|
||||
// On mobile, show the file picker dialog
|
||||
this.showImagePicker = true;
|
||||
}
|
||||
}
|
||||
|
||||
private handleCloseFilePicker() {
|
||||
|
|
@ -1028,12 +1037,12 @@ export class SessionView extends LitElement {
|
|||
box-shadow: none !important;
|
||||
}
|
||||
session-view:focus {
|
||||
outline: 2px solid #00ff88 !important;
|
||||
outline: 2px solid rgb(16 185 129) !important;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
class="flex flex-col bg-black font-mono relative"
|
||||
class="flex flex-col bg-dark-bg font-mono relative"
|
||||
style="height: 100vh; height: 100dvh; outline: none !important; box-shadow: none !important;"
|
||||
>
|
||||
<!-- Session Header -->
|
||||
|
|
@ -1066,7 +1075,7 @@ export class SessionView extends LitElement {
|
|||
|
||||
<!-- Enhanced Terminal Container -->
|
||||
<div
|
||||
class="${this.terminalContainerHeight === '100%' ? 'flex-1' : ''} bg-black overflow-hidden min-h-0 relative border-t border-dark-border shadow-inner ${
|
||||
class="${this.terminalContainerHeight === '100%' ? 'flex-1' : ''} bg-dark-bg overflow-hidden min-h-0 relative ${
|
||||
this.session?.status === 'exited' ? 'session-exited opacity-90' : ''
|
||||
}"
|
||||
id="terminal-container"
|
||||
|
|
@ -1281,12 +1290,23 @@ export class SessionView extends LitElement {
|
|||
${
|
||||
this.isDragOver
|
||||
? html`
|
||||
<div class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 pointer-events-none">
|
||||
<div class="bg-dark-bg-secondary border-2 border-dashed border-terminal-green text-terminal-green rounded-lg p-8 text-center">
|
||||
<div class="text-6xl mb-4">📁</div>
|
||||
<div class="text-xl font-semibold mb-2">Drop files here</div>
|
||||
<div class="text-sm opacity-80">Files will be uploaded and the path sent to terminal</div>
|
||||
<div class="text-xs opacity-60 mt-2">Or press CMD+V to paste from clipboard</div>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-90 backdrop-blur-sm flex items-center justify-center z-50 pointer-events-none animate-fade-in">
|
||||
<div class="bg-dark-bg-elevated border-2 border-dashed border-accent-primary rounded-xl p-10 text-center max-w-md mx-4 shadow-2xl animate-scale-in">
|
||||
<div class="relative mb-6">
|
||||
<div class="w-24 h-24 mx-auto bg-gradient-to-br from-accent-primary to-accent-primary-light rounded-full flex items-center justify-center shadow-glow-primary">
|
||||
<svg class="w-12 h-12 text-dark-bg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-gradient-to-r from-transparent via-accent-primary to-transparent opacity-50"></div>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-dark-text mb-3">Drop files here</h3>
|
||||
<p class="text-sm text-dark-text-muted mb-4">Files will be uploaded and the path sent to terminal</p>
|
||||
<div class="inline-flex items-center gap-2 text-xs text-dark-text-dim bg-dark-bg-secondary px-4 py-2 rounded-lg">
|
||||
<span class="opacity-75">Or press</span>
|
||||
<kbd class="px-2 py-1 bg-dark-bg-tertiary border border-dark-border rounded text-accent-primary font-mono text-xs">⌘V</kbd>
|
||||
<span class="opacity-75">to paste from clipboard</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
|
|
|||
|
|
@ -49,10 +49,10 @@ export class CtrlAlphaOverlay extends LitElement {
|
|||
style="background: black; border: 1px solid #569cd6; border-radius: 8px; padding: 10px; margin-bottom: ${this.keyboardHeight > 0 ? `${this.keyboardHeight + 180}px` : 'calc(env(keyboard-inset-height, 0px) + 180px)'};/* 180px = estimated quick keyboard height (3 rows) */"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
>
|
||||
<div class="text-vs-user text-center mb-2 font-bold">Ctrl + Key</div>
|
||||
<div class="text-primary text-center mb-2 font-bold">Ctrl + Key</div>
|
||||
|
||||
<!-- Help text -->
|
||||
<div class="text-xs text-vs-muted text-center mb-3 opacity-70">
|
||||
<div class="text-xs text-dark-text-muted text-center mb-3 opacity-70">
|
||||
Build sequences like ctrl+c ctrl+c
|
||||
</div>
|
||||
|
||||
|
|
@ -60,9 +60,9 @@ export class CtrlAlphaOverlay extends LitElement {
|
|||
${
|
||||
this.ctrlSequence.length > 0
|
||||
? html`
|
||||
<div class="text-center mb-4 p-2 border border-vs-muted rounded bg-vs-bg">
|
||||
<div class="text-xs text-vs-muted mb-1">Current sequence:</div>
|
||||
<div class="text-sm text-vs-accent font-bold">
|
||||
<div class="text-center mb-4 p-2 border border-dark-border rounded bg-dark-bg">
|
||||
<div class="text-xs text-dark-text-muted mb-1">Current sequence:</div>
|
||||
<div class="text-sm text-primary font-bold">
|
||||
${this.ctrlSequence.map((letter) => `Ctrl+${letter}`).join(' ')}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -112,7 +112,7 @@ export class CtrlAlphaOverlay extends LitElement {
|
|||
</div>
|
||||
|
||||
<!-- Common shortcuts info -->
|
||||
<div class="text-xs text-vs-muted text-center mb-3">
|
||||
<div class="text-xs text-dark-text-muted text-center mb-3">
|
||||
<div>Common: C=interrupt, X=exit, O=save, W=search</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ export class SessionHeader extends LitElement {
|
|||
return html`
|
||||
<!-- Enhanced Header with gradient background -->
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-dark-border text-sm min-w-0 bg-gradient-to-r from-dark-bg-secondary to-dark-bg-tertiary p-4 shadow-sm"
|
||||
style="padding-top: max(1rem, calc(1rem + env(safe-area-inset-top))); padding-left: max(1rem, env(safe-area-inset-left)); padding-right: max(1rem, env(safe-area-inset-right));"
|
||||
class="flex items-center justify-between border-b border-dark-border text-sm min-w-0 bg-gradient-to-r from-dark-bg-secondary to-dark-bg-tertiary px-4 py-2 shadow-sm"
|
||||
style="padding-top: max(0.5rem, env(safe-area-inset-top)); padding-left: max(1rem, env(safe-area-inset-left)); padding-right: max(1rem, env(safe-area-inset-right));"
|
||||
>
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<!-- Sidebar Toggle and Create Session Buttons (shown when sidebar is collapsed) -->
|
||||
|
|
@ -118,7 +118,7 @@ export class SessionHeader extends LitElement {
|
|||
this.showBackButton
|
||||
? html`
|
||||
<button
|
||||
class="btn-secondary font-mono text-xs px-3 py-1 flex-shrink-0"
|
||||
class="bg-dark-bg-elevated border border-dark-border rounded-lg px-3 py-1.5 font-mono text-xs text-dark-text-muted transition-all duration-200 hover:text-accent-primary hover:bg-dark-surface-hover hover:border-accent-primary hover:shadow-sm flex-shrink-0"
|
||||
@click=${() => this.onBack?.()}
|
||||
>
|
||||
Back
|
||||
|
|
|
|||
|
|
@ -51,29 +51,29 @@ export class WidthSelector extends LitElement {
|
|||
|
||||
return html`
|
||||
<div
|
||||
class="width-selector-container absolute top-8 right-0 bg-dark-bg-secondary border border-dark-border rounded-md shadow-lg z-50 min-w-48"
|
||||
class="width-selector-container absolute top-full mt-2 right-0 bg-dark-bg-elevated border border-dark-border rounded-lg shadow-elevated z-50 min-w-[280px] animate-fade-in"
|
||||
>
|
||||
<div class="p-2">
|
||||
<div class="text-xs text-dark-text-muted mb-2 px-2">Terminal Width</div>
|
||||
<div class="p-4">
|
||||
<div class="text-sm font-semibold text-dark-text mb-3">Terminal Width</div>
|
||||
${COMMON_TERMINAL_WIDTHS.map(
|
||||
(width) => html`
|
||||
<button
|
||||
class="w-full text-left px-2 py-1 text-xs hover:bg-dark-border rounded-sm flex justify-between items-center
|
||||
class="w-full text-left px-3 py-2 text-sm rounded-md flex justify-between items-center transition-all duration-200
|
||||
${
|
||||
this.terminalMaxCols === width.value
|
||||
? 'bg-dark-border text-accent-green'
|
||||
: 'text-dark-text'
|
||||
? 'bg-accent-primary bg-opacity-10 text-accent-primary border border-accent-primary'
|
||||
: 'text-dark-text hover:bg-dark-surface-hover hover:text-dark-text-bright border border-transparent'
|
||||
}"
|
||||
@click=${() => this.onWidthSelect?.(width.value)}
|
||||
>
|
||||
<span class="font-mono">${width.label}</span>
|
||||
<span class="text-dark-text-muted text-xs">${width.description}</span>
|
||||
<span class="font-mono font-medium">${width.label}</span>
|
||||
<span class="text-dark-text-muted text-xs ml-4">${width.description}</span>
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
<div class="border-t border-dark-border mt-2 pt-2">
|
||||
<div class="text-xs text-dark-text-muted mb-1 px-2">Custom (20-500)</div>
|
||||
<div class="flex gap-1">
|
||||
<div class="border-t border-dark-border mt-3 pt-3">
|
||||
<div class="text-sm font-semibold text-dark-text mb-2">Custom (20-500)</div>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min="20"
|
||||
|
|
@ -83,10 +83,17 @@ export class WidthSelector extends LitElement {
|
|||
@input=${this.handleCustomWidthInput}
|
||||
@keydown=${this.handleCustomWidthKeydown}
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
class="flex-1 bg-dark-bg border border-dark-border rounded px-2 py-1 text-xs font-mono text-dark-text"
|
||||
class="flex-1 bg-dark-bg-secondary border border-dark-border rounded-md px-3 py-2 text-sm font-mono text-dark-text placeholder:text-dark-text-dim focus:border-accent-primary focus:shadow-glow-primary-sm transition-all"
|
||||
/>
|
||||
<button
|
||||
class="btn-secondary text-xs px-2 py-1"
|
||||
class="px-4 py-2 rounded-md text-sm font-medium transition-all duration-200
|
||||
${
|
||||
!this.customWidth ||
|
||||
Number.parseInt(this.customWidth) < 20 ||
|
||||
Number.parseInt(this.customWidth) > 500
|
||||
? 'bg-dark-bg-secondary border border-dark-border text-dark-text-muted cursor-not-allowed'
|
||||
: 'bg-accent-primary text-dark-bg hover:bg-accent-primary-light active:scale-95'
|
||||
}"
|
||||
@click=${this.handleCustomWidthSubmit}
|
||||
?disabled=${
|
||||
!this.customWidth ||
|
||||
|
|
@ -98,28 +105,47 @@ export class WidthSelector extends LitElement {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-dark-border mt-2 pt-2">
|
||||
<div class="text-xs text-dark-text-muted mb-2 px-2">Font Size</div>
|
||||
<div class="flex items-center gap-2 px-2">
|
||||
<div class="border-t border-dark-border mt-3 pt-3">
|
||||
<div class="text-sm font-semibold text-dark-text mb-3">Font Size</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
class="btn-secondary text-xs px-2 py-1"
|
||||
class="w-10 h-10 rounded-md border transition-all duration-200 flex items-center justify-center
|
||||
${
|
||||
this.terminalFontSize <= 8
|
||||
? 'border-dark-border bg-dark-bg-secondary text-dark-text-muted cursor-not-allowed'
|
||||
: 'border-dark-border bg-dark-bg-elevated text-dark-text hover:border-accent-primary hover:text-accent-primary active:scale-95'
|
||||
}"
|
||||
@click=${() => this.onFontSizeChange?.(this.terminalFontSize - 1)}
|
||||
?disabled=${this.terminalFontSize <= 8}
|
||||
>
|
||||
−
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="font-mono text-xs text-dark-text min-w-8 text-center">
|
||||
<span class="font-mono text-lg font-medium text-dark-text min-w-[60px] text-center">
|
||||
${this.terminalFontSize}px
|
||||
</span>
|
||||
<button
|
||||
class="btn-secondary text-xs px-2 py-1"
|
||||
class="w-10 h-10 rounded-md border transition-all duration-200 flex items-center justify-center
|
||||
${
|
||||
this.terminalFontSize >= 32
|
||||
? 'border-dark-border bg-dark-bg-secondary text-dark-text-muted cursor-not-allowed'
|
||||
: 'border-dark-border bg-dark-bg-elevated text-dark-text hover:border-accent-primary hover:text-accent-primary active:scale-95'
|
||||
}"
|
||||
@click=${() => this.onFontSizeChange?.(this.terminalFontSize + 1)}
|
||||
?disabled=${this.terminalFontSize >= 32}
|
||||
>
|
||||
+
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn-ghost text-xs px-2 py-1 ml-auto"
|
||||
class="ml-auto px-3 py-2 rounded-md text-sm transition-all duration-200
|
||||
${
|
||||
this.terminalFontSize === 14
|
||||
? 'text-dark-text-muted cursor-not-allowed'
|
||||
: 'text-dark-text-muted hover:text-dark-text hover:bg-dark-surface-hover'
|
||||
}"
|
||||
@click=${() => this.onFontSizeChange?.(14)}
|
||||
?disabled=${this.terminalFontSize === 14}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ export class SidebarHeader extends HeaderBase {
|
|||
|
||||
return html`
|
||||
<div
|
||||
class="app-header sidebar-header bg-dark-bg-secondary border-b border-dark-border p-3"
|
||||
style="padding-top: max(0.75rem, calc(0.75rem + env(safe-area-inset-top)));"
|
||||
class="app-header sidebar-header bg-gradient-to-r from-dark-bg-secondary to-dark-bg-tertiary border-b border-dark-border px-4 py-2 shadow-sm"
|
||||
style="padding-top: max(0.625rem, env(safe-area-inset-top));"
|
||||
>
|
||||
<!-- Compact layout for sidebar -->
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -44,7 +44,7 @@ export class SidebarHeader extends HeaderBase {
|
|||
<terminal-icon size="20"></terminal-icon>
|
||||
<div class="min-w-0">
|
||||
<h1
|
||||
class="text-sm font-bold text-accent-green font-mono group-hover:underline truncate"
|
||||
class="text-sm font-bold text-accent-primary font-mono group-hover:underline truncate"
|
||||
>
|
||||
VibeTunnel
|
||||
</h1>
|
||||
|
|
@ -54,8 +54,8 @@ export class SidebarHeader extends HeaderBase {
|
|||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Action buttons group -->
|
||||
<div class="flex items-center gap-1 flex-shrink-0">
|
||||
<!-- Action buttons group with consistent styling -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<!-- Notification button -->
|
||||
<notification-status
|
||||
@open-settings=${() => this.dispatchEvent(new CustomEvent('open-settings'))}
|
||||
|
|
@ -63,7 +63,7 @@ export class SidebarHeader extends HeaderBase {
|
|||
|
||||
<!-- File Browser button -->
|
||||
<button
|
||||
class="p-2 text-dark-text border border-dark-border hover:border-accent-green hover:text-accent-green rounded-lg transition-all duration-200 flex-shrink-0"
|
||||
class="p-2 text-dark-text-muted bg-dark-bg-elevated border border-dark-border hover:border-accent-primary hover:text-accent-primary rounded-md transition-all duration-200 flex-shrink-0"
|
||||
@click=${() => this.dispatchEvent(new CustomEvent('open-file-browser'))}
|
||||
title="Browse Files (⌘O)"
|
||||
>
|
||||
|
|
@ -74,11 +74,11 @@ export class SidebarHeader extends HeaderBase {
|
|||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Create Session button -->
|
||||
<!-- Create Session button with primary styling -->
|
||||
<button
|
||||
class="p-2 text-accent-green border border-accent-green hover:bg-accent-green hover:text-dark-bg rounded-lg transition-all duration-200 flex-shrink-0"
|
||||
class="p-2 text-accent-primary bg-accent-primary bg-opacity-10 border border-accent-primary hover:bg-opacity-20 rounded-md transition-all duration-200 flex-shrink-0"
|
||||
@click=${this.handleCreateSession}
|
||||
title="Create New Session"
|
||||
title="Create New Session (⌘K)"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"/>
|
||||
|
|
|
|||
|
|
@ -433,8 +433,8 @@ export class TerminalQuickKeys extends LitElement {
|
|||
|
||||
/* The actual bar with buttons */
|
||||
.quick-keys-bar {
|
||||
background: rgb(17, 17, 17);
|
||||
border-top: 1px solid rgb(51, 51, 51);
|
||||
background: #0a0a0a;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
padding: 0.5rem 0.25rem;
|
||||
/* Prevent iOS from adding its own styling */
|
||||
-webkit-appearance: none;
|
||||
|
|
@ -455,12 +455,12 @@ export class TerminalQuickKeys extends LitElement {
|
|||
|
||||
/* Modifier key styling */
|
||||
.modifier-key {
|
||||
background-color: #1a1a1a;
|
||||
background-color: #141414;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.modifier-key:hover {
|
||||
background-color: #2a2a2a;
|
||||
background-color: #1f1f1f;
|
||||
}
|
||||
|
||||
/* Arrow key styling */
|
||||
|
|
@ -471,23 +471,23 @@ export class TerminalQuickKeys extends LitElement {
|
|||
|
||||
/* Combo key styling (like ^C, ^Z) */
|
||||
.combo-key {
|
||||
background-color: #1e1e1e;
|
||||
background-color: #141414;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.combo-key:hover {
|
||||
background-color: #2e2e2e;
|
||||
background-color: #1f1f1f;
|
||||
}
|
||||
|
||||
/* Special key styling (like ABC) */
|
||||
.special-key {
|
||||
background-color: rgb(0, 122, 255);
|
||||
border-color: rgb(0, 122, 255);
|
||||
background-color: rgb(16, 185, 129);
|
||||
border-color: rgb(16, 185, 129);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.special-key:hover {
|
||||
background-color: rgb(0, 100, 220);
|
||||
background-color: rgb(5, 150, 105);
|
||||
}
|
||||
|
||||
/* Function key styling */
|
||||
|
|
@ -501,22 +501,22 @@ export class TerminalQuickKeys extends LitElement {
|
|||
|
||||
/* Toggle button styling */
|
||||
.toggle-key {
|
||||
background-color: #2a2a2a;
|
||||
background-color: #1f1f1f;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.toggle-key:hover {
|
||||
background-color: #3a3a3a;
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.toggle-key.active {
|
||||
background-color: rgb(0, 122, 255);
|
||||
border-color: rgb(0, 122, 255);
|
||||
background-color: rgb(16, 185, 129);
|
||||
border-color: rgb(16, 185, 129);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toggle-key.active:hover {
|
||||
background-color: rgb(0, 100, 220);
|
||||
background-color: rgb(5, 150, 105);
|
||||
}
|
||||
|
||||
/* Ctrl shortcut button styling */
|
||||
|
|
|
|||
|
|
@ -79,6 +79,9 @@ describe('Terminal', () => {
|
|||
// Now terminal should be created
|
||||
const terminal = (element as unknown as { terminal: MockTerminal }).terminal;
|
||||
expect(terminal).toBeDefined();
|
||||
|
||||
// Should call scrollToTop on initialization
|
||||
expect(terminal.scrollToTop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle custom dimensions', async () => {
|
||||
|
|
|
|||
|
|
@ -238,6 +238,12 @@ export class Terminal extends LitElement {
|
|||
this.setupResize();
|
||||
this.setupScrolling();
|
||||
|
||||
// Ensure terminal starts at the top
|
||||
this.viewportY = 0;
|
||||
if (this.terminal) {
|
||||
this.terminal.scrollToTop();
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
} catch (error: unknown) {
|
||||
logger.error('failed to initialize terminal:', error);
|
||||
|
|
@ -283,7 +289,7 @@ export class Terminal extends LitElement {
|
|||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#00ff00',
|
||||
cursor: '#10B981',
|
||||
cursorAccent: '#1e1e1e',
|
||||
// Standard 16 colors (0-15) - using proper xterm colors
|
||||
black: '#000000',
|
||||
|
|
@ -938,7 +944,7 @@ export class Terminal extends LitElement {
|
|||
|
||||
// Apply cursor styling after inverse to ensure it takes precedence
|
||||
if (isCursor) {
|
||||
style += `background-color: #23d18b;`;
|
||||
style += `background-color: #10B981;`;
|
||||
}
|
||||
|
||||
// Handle invisible text
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export class VibeLogo extends LitElement {
|
|||
<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>
|
||||
>${coloredLeft} <span class="text-primary">VibeTunnel</span> ${coloredRight}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
/* Default focus styles */
|
||||
:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(0, 255, 136, 0.3);
|
||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
/* iOS Safe Area Support */
|
||||
|
|
@ -65,45 +65,84 @@
|
|||
@layer components {
|
||||
/* Glowing terminal icon */
|
||||
.terminal-icon {
|
||||
@apply text-accent-green;
|
||||
filter: drop-shadow(0 0 10px rgba(0, 255, 136, 0.6));
|
||||
@apply text-primary;
|
||||
filter: drop-shadow(0 0 10px rgba(16, 185, 129, 0.6));
|
||||
}
|
||||
|
||||
/* Input fields with glow effect */
|
||||
/* Input fields with unified style */
|
||||
.input-field {
|
||||
@apply bg-[#121212] border border-dark-border rounded-lg px-4 py-3 text-dark-text w-full;
|
||||
@apply bg-dark-bg-secondary border border-dark-border rounded-lg px-4 py-3 text-dark-text w-full;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
@apply hover:border-accent-green-darker focus:border-accent-green;
|
||||
@apply focus:shadow-glow-green-sm;
|
||||
@apply text-center placeholder:text-center;
|
||||
@apply hover:border-primary/50 focus:border-primary;
|
||||
@apply focus:shadow-glow-sm focus:outline-none;
|
||||
@apply text-left placeholder:text-dark-text-muted;
|
||||
}
|
||||
|
||||
/* Unified button styles */
|
||||
.btn {
|
||||
@apply font-medium rounded-lg transition-all duration-200 ease-in-out;
|
||||
@apply focus:outline-none focus:ring-2 focus:ring-primary/50;
|
||||
@apply disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@apply btn px-3 py-1.5 text-sm;
|
||||
}
|
||||
|
||||
.btn-md {
|
||||
@apply btn px-4 py-2;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
@apply btn px-6 py-3 text-lg;
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.btn-primary {
|
||||
@apply bg-accent-green text-dark-bg font-medium px-6 py-3 rounded-lg;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
@apply hover:bg-accent-green-light hover:shadow-glow-green;
|
||||
@apply btn-md bg-primary text-black;
|
||||
@apply hover:bg-primary-hover hover:shadow-glow;
|
||||
@apply active:scale-95;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply border border-accent-green text-accent-green px-6 py-3 rounded-lg;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
@apply hover:bg-accent-green hover:text-dark-bg hover:shadow-glow-green;
|
||||
@apply btn-md border border-primary text-primary;
|
||||
@apply hover:bg-primary/10 hover:border-primary-hover hover:shadow-glow-sm;
|
||||
@apply active:scale-95;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply text-dark-text-muted px-4 py-2 rounded-lg;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
@apply btn-md text-dark-text-muted;
|
||||
@apply hover:text-dark-text hover:bg-dark-bg-tertiary;
|
||||
}
|
||||
|
||||
/* Enhanced card styles */
|
||||
.card {
|
||||
@apply bg-dark-bg-secondary border border-dark-border rounded-lg p-0;
|
||||
@apply transition-all duration-200 ease-in-out shadow-sm;
|
||||
@apply hover:bg-dark-bg-tertiary hover:border-dark-border-light hover:shadow-card-hover;
|
||||
@apply transition-all duration-300 ease-out shadow-sm;
|
||||
@apply hover:border-dark-border-light hover:shadow-lg hover:-translate-y-1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* Ensure cards have proper z-index for hover */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Card glow effect for active sessions */
|
||||
.card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background: radial-gradient(circle at 50% 50%, transparent 60%, rgba(16, 185, 129, 0.1) 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-out;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-elevated {
|
||||
|
|
@ -118,7 +157,7 @@
|
|||
}
|
||||
|
||||
.status-badge-success {
|
||||
@apply bg-accent-green bg-opacity-20 text-accent-green;
|
||||
@apply bg-status-success bg-opacity-20 text-status-success;
|
||||
}
|
||||
|
||||
.status-badge-warning {
|
||||
|
|
@ -137,12 +176,12 @@
|
|||
.quick-start-btn {
|
||||
@apply bg-dark-bg-tertiary border border-dark-border rounded-lg px-4 py-3 h-12;
|
||||
@apply transition-all duration-200 ease-in-out text-dark-text-muted;
|
||||
@apply hover:border-accent-primary hover:text-accent-primary hover:shadow-glow-primary-sm;
|
||||
@apply hover:border-primary hover:text-primary hover:shadow-glow-sm;
|
||||
@apply active:scale-95;
|
||||
}
|
||||
|
||||
.quick-start-btn.active {
|
||||
@apply bg-accent-primary text-dark-bg border-accent-primary shadow-glow-primary-sm;
|
||||
@apply bg-primary text-black border-primary shadow-glow-sm;
|
||||
}
|
||||
|
||||
/* Modal backdrop */
|
||||
|
|
@ -168,14 +207,15 @@
|
|||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
grid-auto-rows: 400px;
|
||||
gap: 1.25rem;
|
||||
padding: 0 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
/* Enable smooth grid transitions */
|
||||
transition: grid-template-columns 0.3s ease-out;
|
||||
}
|
||||
|
||||
.session-flex-responsive > * {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
/* Allow overflow for hover effects */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
|
|
@ -255,6 +295,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Width selector specific styles */
|
||||
.width-selector-container {
|
||||
@apply backdrop-blur-sm;
|
||||
}
|
||||
|
||||
/* Ensure width selector button doesn't get clipped */
|
||||
.width-selector-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Micro-interactions and animations */
|
||||
@layer utilities {
|
||||
/* Smooth transitions for interactive elements */
|
||||
|
|
@ -280,17 +330,21 @@
|
|||
}
|
||||
|
||||
/* Hover glow effect */
|
||||
.hover-glow-primary:hover {
|
||||
@apply shadow-glow-primary;
|
||||
.hover-glow:hover {
|
||||
@apply shadow-glow;
|
||||
}
|
||||
|
||||
.hover-glow-green:hover {
|
||||
@apply shadow-glow-green;
|
||||
.hover-glow-sm:hover {
|
||||
@apply shadow-glow-sm;
|
||||
}
|
||||
|
||||
.hover-glow-lg:hover {
|
||||
@apply shadow-glow-lg;
|
||||
}
|
||||
|
||||
/* Focus within styles */
|
||||
.focus-within-glow:focus-within {
|
||||
@apply shadow-glow-primary-sm ring-2 ring-accent-primary ring-opacity-50;
|
||||
@apply shadow-glow-sm ring-2 ring-primary ring-opacity-50;
|
||||
}
|
||||
|
||||
/* Smooth color transitions */
|
||||
|
|
@ -714,11 +768,8 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
|
|||
/* Enhanced terminal container styling */
|
||||
.terminal-container {
|
||||
color: #e4e4e4;
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
background-color: #0a0a0a;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Terminal line styling */
|
||||
|
|
@ -773,7 +824,7 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
|
|||
/* Enhanced cursor styling */
|
||||
.terminal-char.cursor {
|
||||
animation: cursor-blink 1s infinite;
|
||||
background-color: #00D9FF;
|
||||
background-color: #10B981;
|
||||
color: #0a0a0a;
|
||||
}
|
||||
|
||||
|
|
@ -781,12 +832,12 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
|
|||
0%,
|
||||
50% {
|
||||
opacity: 1;
|
||||
background-color: #00D9FF;
|
||||
background-color: #10B981;
|
||||
}
|
||||
51%,
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
background-color: #00D9FF;
|
||||
background-color: #10B981;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -804,8 +855,8 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
|
|||
|
||||
/* Enhanced terminal focus indicator */
|
||||
.terminal-focused {
|
||||
box-shadow: 0 0 0 2px #00D9FF, 0 0 20px rgba(0, 217, 255, 0.3);
|
||||
border-color: #00D9FF !important;
|
||||
box-shadow: 0 0 0 2px #10B981, 0 0 20px rgba(16, 185, 129, 0.3);
|
||||
border-color: #10B981 !important;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
|
|
@ -814,17 +865,17 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
|
|||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 217, 255, 0.1);
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid #00D9FF;
|
||||
color: #00D9FF;
|
||||
border: 1px solid #10B981;
|
||||
color: #10B981;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
z-index: 1000;
|
||||
animation: slideInFromTop 0.3s ease-out;
|
||||
box-shadow: 0 2px 8px rgba(0, 217, 255, 0.2);
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
@keyframes slideInFromTop {
|
||||
|
|
@ -1161,8 +1212,8 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
|
|||
/* Hover state for both buttons */
|
||||
.scroll-to-bottom:hover,
|
||||
.keyboard-button:hover {
|
||||
@apply bg-dark-bg/40 border-accent-green text-accent-green;
|
||||
@apply -translate-y-0.5 shadow-glow-green-sm;
|
||||
@apply bg-dark-bg/40 border-primary text-primary;
|
||||
@apply -translate-y-0.5 shadow-glow-sm;
|
||||
backdrop-filter: blur(16px) saturate(1.8);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(1.8);
|
||||
}
|
||||
|
|
@ -1228,10 +1279,10 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
|
|||
|
||||
.fit-toggle:hover {
|
||||
background: rgba(26, 26, 26, 1);
|
||||
border-color: #00ff88;
|
||||
color: #00ff88;
|
||||
border-color: #10B981;
|
||||
color: #10B981;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.fit-toggle:active {
|
||||
|
|
@ -1239,9 +1290,9 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
|
|||
}
|
||||
|
||||
.fit-toggle.active {
|
||||
border-color: #00ff88;
|
||||
color: #00ff88;
|
||||
box-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
|
||||
border-color: #10B981;
|
||||
color: #10B981;
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
/* View Transitions */
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ module.exports = {
|
|||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Enhanced Dark theme colors with better depth
|
||||
// Unified Dark theme colors with consistent depth
|
||||
"dark-bg": "#0a0a0a",
|
||||
"dark-bg-secondary": "#141414",
|
||||
"dark-bg-tertiary": "#1f1f1f",
|
||||
|
|
@ -15,64 +15,52 @@ module.exports = {
|
|||
"dark-border-light": "#3a3a3a",
|
||||
"dark-border-focus": "#4a4a4a",
|
||||
|
||||
// Enhanced Text colors
|
||||
// Text colors
|
||||
"dark-text": "#e4e4e4",
|
||||
"dark-text-bright": "#ffffff",
|
||||
"dark-text-muted": "#888888",
|
||||
"dark-text-dim": "#666666",
|
||||
"dark-text-muted": "#a3a3a3",
|
||||
"dark-text-dim": "#737373",
|
||||
|
||||
// Modern accent colors - Cyan/Teal primary
|
||||
"accent-primary": "#00D9FF",
|
||||
"accent-primary-dark": "#00B8E6",
|
||||
"accent-primary-darker": "#0096CC",
|
||||
"accent-primary-light": "#33E1FF",
|
||||
"accent-primary-glow": "#00D9FF66",
|
||||
// Unified accent color - Vibrant teal-green
|
||||
"primary": "#10B981",
|
||||
"primary-hover": "#059669",
|
||||
"primary-dark": "#047857",
|
||||
"primary-light": "#34D399",
|
||||
"primary-muted": "#10B98133",
|
||||
"primary-glow": "#10B98166",
|
||||
|
||||
// Green accent colors (success/active)
|
||||
"accent-green": "#4ADE80",
|
||||
"accent-green-dark": "#22C55E",
|
||||
"accent-green-darker": "#16A34A",
|
||||
"accent-green-light": "#86EFAC",
|
||||
"accent-green-glow": "#4ADE8066",
|
||||
// Status colors
|
||||
"status-error": "#EF4444",
|
||||
"status-warning": "#F59E0B",
|
||||
"status-success": "#10B981",
|
||||
"status-info": "#3B82F6",
|
||||
|
||||
// Secondary accent colors
|
||||
"accent-purple": "#A78BFA",
|
||||
"accent-blue": "#60A5FA",
|
||||
"accent-amber": "#FFA726",
|
||||
|
||||
// Enhanced Status colors
|
||||
"status-error": "#FF6B6B",
|
||||
"status-warning": "#FFA726",
|
||||
"status-success": "#4ADE80",
|
||||
"status-info": "#60A5FA",
|
||||
|
||||
// Legacy VS Code theme colors (for compatibility)
|
||||
"vs-bg": "#0a0a0a",
|
||||
"vs-text": "#e4e4e4",
|
||||
"vs-muted": "#7a7a7a",
|
||||
"vs-accent": "#00ff88",
|
||||
"vs-user": "#00ff88",
|
||||
"vs-assistant": "#00ccaa",
|
||||
"vs-warning": "#ffaa44",
|
||||
"vs-function": "#44ffaa",
|
||||
"vs-type": "#00ffcc",
|
||||
"vs-border": "#2a2a2a",
|
||||
"vs-border-light": "#3a3a3a",
|
||||
"vs-bg-secondary": "#1a1a1a",
|
||||
"vs-nav": "#1a1a1a",
|
||||
"vs-nav-hover": "#242424",
|
||||
"vs-nav-active": "#00ff88",
|
||||
"vs-highlight": "#8b6914",
|
||||
// Legacy mappings for gradual migration
|
||||
"accent-primary": "#10B981",
|
||||
"accent-primary-dark": "#059669",
|
||||
"accent-primary-darker": "#047857",
|
||||
"accent-primary-light": "#34D399",
|
||||
"accent-primary-glow": "#10B98166",
|
||||
"accent-green": "#10B981",
|
||||
"accent-green-dark": "#059669",
|
||||
"accent-green-darker": "#047857",
|
||||
"accent-green-light": "#34D399",
|
||||
"accent-green-glow": "#10B98166",
|
||||
},
|
||||
boxShadow: {
|
||||
// Updated glow effects with new colors
|
||||
'glow-primary': '0 0 20px rgba(0, 217, 255, 0.4)',
|
||||
'glow-primary-sm': '0 0 10px rgba(0, 217, 255, 0.3)',
|
||||
'glow-primary-lg': '0 0 30px rgba(0, 217, 255, 0.5)',
|
||||
'glow-green': '0 0 20px rgba(74, 222, 128, 0.4)',
|
||||
'glow-green-sm': '0 0 10px rgba(74, 222, 128, 0.3)',
|
||||
'glow-green-lg': '0 0 30px rgba(74, 222, 128, 0.5)',
|
||||
// New subtle shadows
|
||||
// Unified glow effects with primary color
|
||||
'glow': '0 0 20px rgba(16, 185, 129, 0.4)',
|
||||
'glow-sm': '0 0 10px rgba(16, 185, 129, 0.3)',
|
||||
'glow-lg': '0 0 30px rgba(16, 185, 129, 0.5)',
|
||||
'glow-intense': '0 0 40px rgba(16, 185, 129, 0.6)',
|
||||
// Legacy mappings
|
||||
'glow-primary': '0 0 20px rgba(16, 185, 129, 0.4)',
|
||||
'glow-primary-sm': '0 0 10px rgba(16, 185, 129, 0.3)',
|
||||
'glow-primary-lg': '0 0 30px rgba(16, 185, 129, 0.5)',
|
||||
'glow-green': '0 0 20px rgba(16, 185, 129, 0.4)',
|
||||
'glow-green-sm': '0 0 10px rgba(16, 185, 129, 0.3)',
|
||||
'glow-green-lg': '0 0 30px rgba(16, 185, 129, 0.5)',
|
||||
// Subtle shadows for depth
|
||||
'card': '0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.4)',
|
||||
'card-hover': '0 4px 6px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.4)',
|
||||
'elevated': '0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2)',
|
||||
|
|
|
|||
Loading…
Reference in a new issue