Enhance VibeTunnel web interface with modern visual design (#177)

This commit is contained in:
Peter Steinberger 2025-07-01 17:57:57 +01:00 committed by GitHub
parent b452251a47
commit fcda54a5f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 632 additions and 301 deletions

View file

@ -113,6 +113,11 @@ export class VibeTunnelApp extends LitElement {
this.hasActiveOverlay = this.hasActiveOverlay =
this.showFileBrowser || this.showCreateModal || this.showSSHKeyManager || this.showSettings; 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() { disconnectedCallback() {
@ -1155,6 +1160,54 @@ export class VibeTunnelApp extends LitElement {
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !('MSStream' in window); 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 { private get isInSidebarDismissMode(): boolean {
if (!this.mediaState.isMobile || !this.shouldShowMobileOverlay) return false; if (!this.mediaState.isMobile || !this.shouldShowMobileOverlay) return false;
@ -1372,13 +1425,13 @@ export class VibeTunnelApp extends LitElement {
@error=${this.handleError} @error=${this.handleError}
></session-create-form> ></session-create-form>
<!-- Version and logs link in bottom right --> <!-- Version and logs link with smart positioning -->
${ ${
this.showLogLink this.showLogLink
? html` ? 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> <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> </div>
` `
: '' : ''

View file

@ -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"> <div class="flex flex-col items-center gap-2 sm:gap-3 mb-4 sm:mb-8">
<terminal-icon <terminal-icon
size="${this.isMobile ? '48' : '56'}" 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> ></terminal-icon>
<h2 class="auth-title text-2xl sm:text-3xl mt-1 sm:mt-2">VibeTunnel</h2> <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> <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="flex flex-col items-center mb-4 sm:mb-6">
<div <div
class="w-24 h-24 sm:w-28 sm:h-28 rounded-full mb-3 sm:mb-4 overflow-hidden" 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 this.userAvatar
@ -361,7 +361,7 @@ export class AuthLogin extends LitElement {
<div class="ssh-key-item p-6 sm:p-8"> <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 justify-between mb-3 sm:mb-4">
<div class="flex items-center gap-2"> <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> <span class="font-mono text-xs sm:text-sm">SSH Key Management</span>
</div> </div>
<button <button

View file

@ -39,7 +39,7 @@ export class AuthQuickKeys extends LitElement {
({ key, label }) => html` ({ key, label }) => html`
<button <button
type="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)} @click=${() => this.handleKeyPress(key)}
> >
${label} ${label}

View file

@ -484,7 +484,7 @@ export class FileBrowser extends LitElement {
let className = 'text-dark-text-muted'; let className = 'text-dark-text-muted';
if (line.startsWith('+')) className = 'text-status-success bg-green-900/20'; 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-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>`; return html`<div class="whitespace-pre ${className}">${line}</div>`;
})} })}
@ -550,7 +550,7 @@ export class FileBrowser extends LitElement {
@input=${this.handlePathInput} @input=${this.handlePathInput}
@keydown=${this.handlePathKeyDown} @keydown=${this.handlePathKeyDown}
@blur=${this.handlePathBlur} @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" placeholder="Enter path and press Enter"
/> />
` `
@ -598,7 +598,7 @@ export class FileBrowser extends LitElement {
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
class="btn-secondary text-xs px-2 py-1 font-mono ${ 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} @click=${this.toggleGitFilter}
title="Show only Git changes" title="Show only Git changes"
@ -607,7 +607,7 @@ export class FileBrowser extends LitElement {
</button> </button>
<button <button
class="btn-secondary text-xs px-2 py-1 font-mono ${ 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} @click=${this.toggleHidden}
title="Show hidden files" 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 class="p-3 hover:bg-dark-bg-lighter cursor-pointer transition-colors flex items-center gap-2
${ ${
this.selectedFile?.path === file.path 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)} @click=${() => this.handleFileClick(file)}
@ -684,7 +684,7 @@ export class FileBrowser extends LitElement {
</span> </span>
<span <span
class="flex-1 text-sm whitespace-nowrap ${ 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)' : ''}" title="${file.name}${file.isSymlink ? ' (symlink)' : ''}"
>${file.name}</span >${file.name}</span
@ -805,7 +805,7 @@ export class FileBrowser extends LitElement {
? html` ? html`
<button <button
class="btn-secondary text-xs px-2 py-1 font-mono ${ 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.isMobile &&
this.selectedFile.type === 'file' && this.selectedFile.type === 'file' &&

View file

@ -1,3 +1,4 @@
// @vitest-environment happy-dom
/** /**
* Unit tests for FilePicker component * Unit tests for FilePicker component
*/ */
@ -63,7 +64,7 @@ describe('FilePicker Component', () => {
const progressText = element.querySelector('span'); const progressText = element.querySelector('span');
expect(progressText?.textContent).toContain('Uploading...'); expect(progressText?.textContent).toContain('Uploading...');
const progressBar = element.querySelector('.bg-blue-500'); const progressBar = element.querySelector('.bg-gradient-to-r');
expect(progressBar).toBeTruthy(); expect(progressBar).toBeTruthy();
}); });
@ -115,7 +116,7 @@ describe('FilePicker Component', () => {
const cancelEventSpy = vi.fn(); const cancelEventSpy = vi.fn();
element.addEventListener('file-cancel', cancelEventSpy); element.addEventListener('file-cancel', cancelEventSpy);
const modal = element.querySelector('.bg-white'); const modal = element.querySelector('.bg-dark-bg-elevated');
expect(modal).toBeTruthy(); expect(modal).toBeTruthy();
modal?.dispatchEvent(new MouseEvent('click', { bubbles: true })); modal?.dispatchEvent(new MouseEvent('click', { bubbles: true }));

View file

@ -35,6 +35,7 @@ export class FilePicker extends LitElement {
@property({ type: Boolean }) visible = false; @property({ type: Boolean }) visible = false;
@property({ type: Boolean }) showPathOption = true; // Whether to show "Send path to terminal" option @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 uploading = false;
@state() private uploadProgress = 0; @state() private uploadProgress = 0;
@ -45,6 +46,20 @@ export class FilePicker extends LitElement {
this.createFileInput(); 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() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
if (this.fileInput) { if (this.fileInput) {
@ -94,6 +109,13 @@ export class FilePicker extends LitElement {
return this.uploadFileToServer(file); 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> { private async uploadFileToServer(file: File): Promise<void> {
this.uploading = true; this.uploading = true;
this.uploadProgress = 0; this.uploadProgress = 0;
@ -187,52 +209,53 @@ export class FilePicker extends LitElement {
} }
render() { render() {
// Always render a container so the file input is available
if (!this.visible) { if (!this.visible) {
return html``; return html`<div style="display: none;"></div>`;
} }
return html` return html`
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" @click=${this.handleCancel}> <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-white dark:bg-gray-800 rounded-lg p-6 m-4 max-w-sm w-full" @click=${(e: Event) => e.stopPropagation()}> <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-lg font-semibold mb-4 text-gray-900 dark:text-white"> <h3 class="text-xl font-bold text-dark-text mb-6">
Select File Select File
</h3> </h3>
${ ${
this.uploading this.uploading
? html` ? html`
<div class="mb-4"> <div class="mb-6">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-3">
<span class="text-sm text-gray-600 dark:text-gray-400">Uploading...</span> <span class="text-sm text-dark-text-muted font-mono">Uploading...</span>
<span class="text-sm text-gray-600 dark:text-gray-400">${Math.round(this.uploadProgress)}%</span> <span class="text-sm text-accent-primary font-mono font-medium">${Math.round(this.uploadProgress)}%</span>
</div> </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 <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}%" style="width: ${this.uploadProgress}%"
></div> ></div>
</div> </div>
</div> </div>
` `
: html` : html`
<div class="space-y-3"> <div class="space-y-4">
<button <button
@click=${this.handleFileClick} @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"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<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> <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> </svg>
<span>Choose File</span> <span class="font-mono">Choose File</span>
</button> </button>
</div> </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 <button
@click=${this.handleCancel} @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} ?disabled=${this.uploading}
> >
Cancel Cancel

View file

@ -370,7 +370,7 @@ describe('SessionCard', () => {
await element.updateComplete; await element.updateComplete;
const card = element.querySelector('.card'); 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 () => { it('should apply opacity when killing', async () => {

View file

@ -224,7 +224,7 @@ export class SessionCard extends LitElement {
this.killing ? 'opacity-60' : '' this.killing ? 'opacity-60' : ''
} ${ } ${
this.isActive && this.session.status === 'running' 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-${ style="view-transition-name: session-${this.session.id}; --session-id: session-${
@ -238,7 +238,7 @@ export class SessionCard extends LitElement {
> >
<!-- Compact Header --> <!-- Compact Header -->
<div <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="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(' ')}"> <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' this.session.status === 'running' || this.session.status === 'exited'
? html` ? html`
<button <button
class="btn-ghost ${ class="p-1 rounded-full transition-all duration-200 disabled:opacity-50 flex-shrink-0 ${
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 ${
this.session.status === 'running' this.session.status === 'running'
? 'hover:bg-status-error' ? 'text-status-error hover:bg-status-error hover:bg-opacity-20'
: 'hover:bg-status-warning' : 'text-status-warning hover:bg-status-warning hover:bg-opacity-20'
}" }"
@click=${this.handleKillClick} @click=${this.handleKillClick}
?disabled=${this.killing} ?disabled=${this.killing}
@ -292,9 +290,10 @@ export class SessionCard extends LitElement {
<!-- Terminal display (main content) --> <!-- Terminal display (main content) -->
<div <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' : '' 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 this.killing
@ -319,18 +318,20 @@ export class SessionCard extends LitElement {
<!-- Compact Footer --> <!-- Compact Footer -->
<div <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"> <div class="flex justify-between items-center min-w-0">
<span <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-status="${this.session.status}"
data-killing="${this.killing}" data-killing="${this.killing}"
> >
<div class="w-2 h-2 rounded-full ${this.getStatusDotColor()}"></div> <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>` ? html`<span class="text-accent-green animate-pulse ml-1">●</span>`
: '' : ''
} }
@ -364,6 +365,19 @@ export class SessionCard extends LitElement {
return this.session.status; 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 { private getStatusColor(): string {
if (this.killing) { if (this.killing) {
return 'text-status-error'; return 'text-status-error';
@ -374,6 +388,19 @@ export class SessionCard extends LitElement {
return this.session.status === 'running' ? 'text-status-success' : 'text-status-warning'; 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 { private getStatusDotColor(): string {
if (this.killing) { if (this.killing) {
return 'bg-status-error animate-pulse'; return 'bg-status-error animate-pulse';
@ -381,6 +408,15 @@ export class SessionCard extends LitElement {
if (this.session.active === false) { if (this.session.active === false) {
return 'bg-dark-text-muted'; 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';
} }
} }

View file

@ -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" 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" style="view-transition-name: create-session-modal"
> >
<div class="p-4 pb-4 mb-3 border-b border-dark-border relative"> <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-accent-green text-lg font-bold">New Session</h2> <h2 class="text-primary text-xl font-bold">New Session</h2>
<button <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} @click=${this.handleCancel}
title="Close" title="Close (Esc)"
aria-label="Close modal" aria-label="Close modal"
> >
<svg <svg
class="w-6 h-6" class="w-5 h-5"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -405,10 +405,10 @@ export class SessionCreateForm extends LitElement {
</button> </button>
</div> </div>
<div class="p-3 sm:p-3 lg:p-4"> <div class="p-6">
<!-- Session Name --> <!-- Session Name -->
<div class="mb-4"> <div class="mb-5">
<label class="form-label">Session Name (Optional):</label> <label class="form-label text-dark-text-muted">Session Name (Optional):</label>
<input <input
type="text" type="text"
class="input-field" class="input-field"
@ -420,8 +420,8 @@ export class SessionCreateForm extends LitElement {
</div> </div>
<!-- Command --> <!-- Command -->
<div class="mb-4"> <div class="mb-5">
<label class="form-label">Command:</label> <label class="form-label text-dark-text-muted">Command:</label>
<input <input
type="text" type="text"
class="input-field" class="input-field"
@ -433,9 +433,9 @@ export class SessionCreateForm extends LitElement {
</div> </div>
<!-- Working Directory --> <!-- Working Directory -->
<div class="mb-4"> <div class="mb-5">
<label class="form-label">Working Directory:</label> <label class="form-label text-dark-text-muted">Working Directory:</label>
<div class="flex gap-4"> <div class="flex gap-2">
<input <input
type="text" type="text"
class="input-field" class="input-field"
@ -445,27 +445,32 @@ export class SessionCreateForm extends LitElement {
?disabled=${this.disabled || this.isCreating} ?disabled=${this.disabled || this.isCreating}
/> />
<button <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} @click=${this.handleBrowse}
?disabled=${this.disabled || this.isCreating} ?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> </button>
</div> </div>
</div> </div>
<!-- Spawn Window Toggle --> <!-- 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"> <div class="flex-1 pr-4">
<span class="text-dark-text text-sm">Spawn window</span> <span class="text-dark-text text-sm font-medium">Spawn window</span>
<p class="text-xs text-dark-text-muted mt-1">Opens native terminal window</p> <p class="text-xs text-dark-text-muted mt-0.5">Opens native terminal window</p>
</div> </div>
<button <button
role="switch" role="switch"
aria-checked="${this.spawnWindow}" aria-checked="${this.spawnWindow}"
@click=${this.handleSpawnWindowChange} @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 ${ 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-accent-green' : 'bg-dark-border' this.spawnWindow ? 'bg-primary' : 'bg-dark-border'
}" }"
?disabled=${this.disabled || this.isCreating} ?disabled=${this.disabled || this.isCreating}
> >
@ -478,10 +483,10 @@ export class SessionCreateForm extends LitElement {
</div> </div>
<!-- Terminal Title Mode --> <!-- 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"> <div class="flex-1 pr-4">
<span class="text-dark-text text-sm">Terminal Title Mode</span> <span class="text-dark-text text-sm font-medium">Terminal Title Mode</span>
<p class="text-xs text-dark-text-muted mt-1 opacity-50"> <p class="text-xs text-dark-text-muted mt-0.5">
${this.getTitleModeDescription()} ${this.getTitleModeDescription()}
</p> </p>
</div> </div>
@ -489,14 +494,14 @@ export class SessionCreateForm extends LitElement {
<select <select
.value=${this.titleMode} .value=${this.titleMode}
@change=${this.handleTitleModeChange} @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" style="min-width: 140px"
?disabled=${this.disabled || this.isCreating} ?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.NONE}" class="bg-dark-bg-secondary 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.FILTER}" class="bg-dark-bg-secondary 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.STATIC}" class="bg-dark-bg-secondary 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.DYNAMIC}" class="bg-dark-bg-secondary text-dark-text" ?selected=${this.titleMode === TitleMode.DYNAMIC}>Dynamic</option>
</select> </select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-dark-text-muted"> <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"> <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> </div>
<!-- Quick Start Section --> <!-- Quick Start Section -->
<div class="mb-4"> <div class="mb-6">
<label class="form-label text-dark-text-muted uppercase text-xs tracking-wider" <label class="form-label text-dark-text-muted uppercase text-xs tracking-wider mb-3"
>Quick Start</label >Quick Start</label
> >
<div class="grid grid-cols-2 gap-3 mt-2"> <div class="grid grid-cols-2 gap-3 mt-2">
@ -518,8 +523,8 @@ export class SessionCreateForm extends LitElement {
@click=${() => this.handleQuickStart(command)} @click=${() => this.handleQuickStart(command)}
class="${ class="${
this.command === command 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-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 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-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} ?disabled=${this.disabled || this.isCreating}
> >
@ -532,16 +537,16 @@ export class SessionCreateForm extends LitElement {
</div> </div>
</div> </div>
<div class="flex gap-4 mt-4"> <div class="flex gap-3 mt-6">
<button <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} @click=${this.handleCancel}
?disabled=${this.isCreating} ?disabled=${this.isCreating}
> >
Cancel Cancel
</button> </button>
<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} @click=${this.handleCreate}
?disabled=${ ?disabled=${
this.disabled || this.disabled ||

View file

@ -148,14 +148,19 @@ export class SessionList extends LitElement {
} }
render() { render() {
const filteredSessions = this.hideExited // Group sessions by status
? this.sessions.filter((session) => session.status !== 'exited') const runningSessions = this.sessions.filter((session) => session.status === 'running');
: this.sessions; 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` 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` ? html`
<div class="text-dark-text-muted text-center py-8"> <div class="text-dark-text-muted text-center py-8">
${ ${
@ -229,9 +234,17 @@ export class SessionList extends LitElement {
</div> </div>
` `
: html` : html`
<div class="${this.compactMode ? 'space-y-2' : 'session-flex-responsive'}"> <!-- 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( ${repeat(
filteredSessions, runningSessions,
(session) => session.id, (session) => session.id,
(session) => html` (session) => html`
${ ${
@ -411,8 +424,142 @@ 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()} ${this.renderExitedControls()}
</div> </div>
@ -427,45 +574,25 @@ export class SessionList extends LitElement {
if (exitedSessions.length === 0 && runningSessions.length === 0) return ''; if (exitedSessions.length === 0 && runningSessions.length === 0) return '';
return html` return html`
<div class="flex flex-col sm:flex-row sm:flex-wrap gap-2 mt-8 pb-4 px-4 w-full"> <div class="sticky bottom-0 border-t border-dark-border bg-dark-bg-secondary p-3 flex flex-wrap gap-2 shadow-lg">
<!-- First group: Show/Hide Exited and Clean Exited (when visible) --> <!-- Control buttons with consistent styling -->
${ ${
exitedSessions.length > 0 exitedSessions.length > 0
? html` ? html`
<div class="flex flex-col gap-2 w-full sm:w-auto">
<!-- Show/Hide Exited button --> <!-- Show/Hide Exited button -->
<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 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-elevated text-dark-text-muted hover:bg-dark-surface-hover hover:text-accent-primary hover:border-accent-primary hover:shadow-sm'
: 'border-dark-border bg-dark-bg-tertiary text-dark-text hover:bg-dark-bg-secondary' : 'border-accent-primary bg-accent-primary bg-opacity-10 text-accent-primary hover:bg-opacity-20 hover:shadow-glow-primary-sm'
}" }"
@click=${() => @click=${() =>
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('hide-exited-change', { detail: !this.hideExited }) new CustomEvent('hide-exited-change', { detail: !this.hideExited })
)} )}
> >
<div class="flex items-center justify-center gap-2 sm:gap-3"> ${this.hideExited ? 'Show' : 'Hide'} Exited
<span class="hidden sm:inline" <span class="text-dark-text-dim">(${exitedSessions.length})</span>
>${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>
</button> </button>
<!-- Clean Exited button (only when Show Exited is active) --> <!-- Clean Exited button (only when Show Exited is active) -->
@ -473,23 +600,15 @@ export class SessionList extends LitElement {
!this.hideExited !this.hideExited
? html` ? html`
<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-[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} @click=${this.handleCleanupExited}
?disabled=${this.cleaningExited} ?disabled=${this.cleaningExited}
> >
<span class="hidden sm:inline" ${this.cleaningExited ? 'Cleaning...' : 'Clean Exited'}
>${
this.cleaningExited
? 'Cleaning...'
: `Clean Exited (${exitedSessions.length})`
}</span
>
<span class="sm:hidden">${this.cleaningExited ? 'Cleaning...' : 'Clean'}</span>
</button> </button>
` `
: '' : ''
} }
</div>
` `
: '' : ''
} }
@ -499,10 +618,10 @@ export class SessionList extends LitElement {
runningSessions.length > 0 runningSessions.length > 0
? html` ? html`
<button <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'))} @click=${() => this.dispatchEvent(new CustomEvent('kill-all-sessions'))}
> >
Kill All (${runningSessions.length}) Kill All <span class="text-dark-text-dim">(${runningSessions.length})</span>
</button> </button>
` `
: '' : ''

View file

@ -761,8 +761,17 @@ export class SessionView extends LitElement {
} }
private handleOpenFilePicker() { private handleOpenFilePicker() {
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; this.showImagePicker = true;
} }
}
private handleCloseFilePicker() { private handleCloseFilePicker() {
this.showImagePicker = false; this.showImagePicker = false;
@ -1028,12 +1037,12 @@ export class SessionView extends LitElement {
box-shadow: none !important; box-shadow: none !important;
} }
session-view:focus { session-view:focus {
outline: 2px solid #00ff88 !important; outline: 2px solid rgb(16 185 129) !important;
outline-offset: -2px; outline-offset: -2px;
} }
</style> </style>
<div <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;" style="height: 100vh; height: 100dvh; outline: none !important; box-shadow: none !important;"
> >
<!-- Session Header --> <!-- Session Header -->
@ -1066,7 +1075,7 @@ export class SessionView extends LitElement {
<!-- Enhanced Terminal Container --> <!-- Enhanced Terminal Container -->
<div <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' : '' this.session?.status === 'exited' ? 'session-exited opacity-90' : ''
}" }"
id="terminal-container" id="terminal-container"
@ -1281,12 +1290,23 @@ export class SessionView extends LitElement {
${ ${
this.isDragOver this.isDragOver
? html` ? html`
<div class="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 pointer-events-none"> <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-secondary border-2 border-dashed border-terminal-green text-terminal-green rounded-lg p-8 text-center"> <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="text-6xl mb-4">📁</div> <div class="relative mb-6">
<div class="text-xl font-semibold mb-2">Drop files here</div> <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">
<div class="text-sm opacity-80">Files will be uploaded and the path sent to terminal</div> <svg class="w-12 h-12 text-dark-bg" fill="currentColor" viewBox="0 0 20 20">
<div class="text-xs opacity-60 mt-2">Or press CMD+V to paste from clipboard</div> <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>
</div> </div>
` `

View file

@ -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) */" 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()} @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 --> <!-- 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 Build sequences like ctrl+c ctrl+c
</div> </div>
@ -60,9 +60,9 @@ export class CtrlAlphaOverlay extends LitElement {
${ ${
this.ctrlSequence.length > 0 this.ctrlSequence.length > 0
? html` ? html`
<div class="text-center mb-4 p-2 border border-vs-muted rounded bg-vs-bg"> <div class="text-center mb-4 p-2 border border-dark-border rounded bg-dark-bg">
<div class="text-xs text-vs-muted mb-1">Current sequence:</div> <div class="text-xs text-dark-text-muted mb-1">Current sequence:</div>
<div class="text-sm text-vs-accent font-bold"> <div class="text-sm text-primary font-bold">
${this.ctrlSequence.map((letter) => `Ctrl+${letter}`).join(' ')} ${this.ctrlSequence.map((letter) => `Ctrl+${letter}`).join(' ')}
</div> </div>
</div> </div>
@ -112,7 +112,7 @@ export class CtrlAlphaOverlay extends LitElement {
</div> </div>
<!-- Common shortcuts info --> <!-- 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>Common: C=interrupt, X=exit, O=save, W=search</div>
</div> </div>

View file

@ -77,8 +77,8 @@ export class SessionHeader extends LitElement {
return html` return html`
<!-- Enhanced Header with gradient background --> <!-- Enhanced Header with gradient background -->
<div <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" 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(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));" 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"> <div class="flex items-center gap-3 min-w-0 flex-1">
<!-- Sidebar Toggle and Create Session Buttons (shown when sidebar is collapsed) --> <!-- Sidebar Toggle and Create Session Buttons (shown when sidebar is collapsed) -->
@ -118,7 +118,7 @@ export class SessionHeader extends LitElement {
this.showBackButton this.showBackButton
? html` ? html`
<button <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?.()} @click=${() => this.onBack?.()}
> >
Back Back

View file

@ -51,29 +51,29 @@ export class WidthSelector extends LitElement {
return html` return html`
<div <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="p-4">
<div class="text-xs text-dark-text-muted mb-2 px-2">Terminal Width</div> <div class="text-sm font-semibold text-dark-text mb-3">Terminal Width</div>
${COMMON_TERMINAL_WIDTHS.map( ${COMMON_TERMINAL_WIDTHS.map(
(width) => html` (width) => html`
<button <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 this.terminalMaxCols === width.value
? 'bg-dark-border text-accent-green' ? 'bg-accent-primary bg-opacity-10 text-accent-primary border border-accent-primary'
: 'text-dark-text' : 'text-dark-text hover:bg-dark-surface-hover hover:text-dark-text-bright border border-transparent'
}" }"
@click=${() => this.onWidthSelect?.(width.value)} @click=${() => this.onWidthSelect?.(width.value)}
> >
<span class="font-mono">${width.label}</span> <span class="font-mono font-medium">${width.label}</span>
<span class="text-dark-text-muted text-xs">${width.description}</span> <span class="text-dark-text-muted text-xs ml-4">${width.description}</span>
</button> </button>
` `
)} )}
<div class="border-t border-dark-border mt-2 pt-2"> <div class="border-t border-dark-border mt-3 pt-3">
<div class="text-xs text-dark-text-muted mb-1 px-2">Custom (20-500)</div> <div class="text-sm font-semibold text-dark-text mb-2">Custom (20-500)</div>
<div class="flex gap-1"> <div class="flex gap-2">
<input <input
type="number" type="number"
min="20" min="20"
@ -83,10 +83,17 @@ export class WidthSelector extends LitElement {
@input=${this.handleCustomWidthInput} @input=${this.handleCustomWidthInput}
@keydown=${this.handleCustomWidthKeydown} @keydown=${this.handleCustomWidthKeydown}
@click=${(e: Event) => e.stopPropagation()} @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 <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} @click=${this.handleCustomWidthSubmit}
?disabled=${ ?disabled=${
!this.customWidth || !this.customWidth ||
@ -98,28 +105,47 @@ export class WidthSelector extends LitElement {
</button> </button>
</div> </div>
</div> </div>
<div class="border-t border-dark-border mt-2 pt-2"> <div class="border-t border-dark-border mt-3 pt-3">
<div class="text-xs text-dark-text-muted mb-2 px-2">Font Size</div> <div class="text-sm font-semibold text-dark-text mb-3">Font Size</div>
<div class="flex items-center gap-2 px-2"> <div class="flex items-center gap-3">
<button <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)} @click=${() => this.onFontSizeChange?.(this.terminalFontSize - 1)}
?disabled=${this.terminalFontSize <= 8} ?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> </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 ${this.terminalFontSize}px
</span> </span>
<button <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)} @click=${() => this.onFontSizeChange?.(this.terminalFontSize + 1)}
?disabled=${this.terminalFontSize >= 32} ?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>
<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)} @click=${() => this.onFontSizeChange?.(14)}
?disabled=${this.terminalFontSize === 14} ?disabled=${this.terminalFontSize === 14}
> >

View file

@ -16,8 +16,8 @@ export class SidebarHeader extends HeaderBase {
return html` return html`
<div <div
class="app-header sidebar-header bg-dark-bg-secondary border-b border-dark-border p-3" 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.75rem, calc(0.75rem + env(safe-area-inset-top)));" style="padding-top: max(0.625rem, env(safe-area-inset-top));"
> >
<!-- Compact layout for sidebar --> <!-- Compact layout for sidebar -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -44,7 +44,7 @@ export class SidebarHeader extends HeaderBase {
<terminal-icon size="20"></terminal-icon> <terminal-icon size="20"></terminal-icon>
<div class="min-w-0"> <div class="min-w-0">
<h1 <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 VibeTunnel
</h1> </h1>
@ -54,8 +54,8 @@ export class SidebarHeader extends HeaderBase {
</div> </div>
</button> </button>
<!-- Action buttons group --> <!-- Action buttons group with consistent styling -->
<div class="flex items-center gap-1 flex-shrink-0"> <div class="flex items-center gap-2 flex-shrink-0">
<!-- Notification button --> <!-- Notification button -->
<notification-status <notification-status
@open-settings=${() => this.dispatchEvent(new CustomEvent('open-settings'))} @open-settings=${() => this.dispatchEvent(new CustomEvent('open-settings'))}
@ -63,7 +63,7 @@ export class SidebarHeader extends HeaderBase {
<!-- File Browser button --> <!-- File Browser button -->
<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'))} @click=${() => this.dispatchEvent(new CustomEvent('open-file-browser'))}
title="Browse Files (⌘O)" title="Browse Files (⌘O)"
> >
@ -74,11 +74,11 @@ export class SidebarHeader extends HeaderBase {
</svg> </svg>
</button> </button>
<!-- Create Session button --> <!-- Create Session button with primary styling -->
<button <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} @click=${this.handleCreateSession}
title="Create New Session" title="Create New Session (⌘K)"
> >
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"> <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"/> <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"/>

View file

@ -433,8 +433,8 @@ export class TerminalQuickKeys extends LitElement {
/* The actual bar with buttons */ /* The actual bar with buttons */
.quick-keys-bar { .quick-keys-bar {
background: rgb(17, 17, 17); background: #0a0a0a;
border-top: 1px solid rgb(51, 51, 51); border-top: 1px solid #2a2a2a;
padding: 0.5rem 0.25rem; padding: 0.5rem 0.25rem;
/* Prevent iOS from adding its own styling */ /* Prevent iOS from adding its own styling */
-webkit-appearance: none; -webkit-appearance: none;
@ -455,12 +455,12 @@ export class TerminalQuickKeys extends LitElement {
/* Modifier key styling */ /* Modifier key styling */
.modifier-key { .modifier-key {
background-color: #1a1a1a; background-color: #141414;
border-color: #444; border-color: #444;
} }
.modifier-key:hover { .modifier-key:hover {
background-color: #2a2a2a; background-color: #1f1f1f;
} }
/* Arrow key styling */ /* Arrow key styling */
@ -471,23 +471,23 @@ export class TerminalQuickKeys extends LitElement {
/* Combo key styling (like ^C, ^Z) */ /* Combo key styling (like ^C, ^Z) */
.combo-key { .combo-key {
background-color: #1e1e1e; background-color: #141414;
border-color: #555; border-color: #555;
} }
.combo-key:hover { .combo-key:hover {
background-color: #2e2e2e; background-color: #1f1f1f;
} }
/* Special key styling (like ABC) */ /* Special key styling (like ABC) */
.special-key { .special-key {
background-color: rgb(0, 122, 255); background-color: rgb(16, 185, 129);
border-color: rgb(0, 122, 255); border-color: rgb(16, 185, 129);
color: white; color: white;
} }
.special-key:hover { .special-key:hover {
background-color: rgb(0, 100, 220); background-color: rgb(5, 150, 105);
} }
/* Function key styling */ /* Function key styling */
@ -501,22 +501,22 @@ export class TerminalQuickKeys extends LitElement {
/* Toggle button styling */ /* Toggle button styling */
.toggle-key { .toggle-key {
background-color: #2a2a2a; background-color: #1f1f1f;
border-color: #666; border-color: #666;
} }
.toggle-key:hover { .toggle-key:hover {
background-color: #3a3a3a; background-color: #2a2a2a;
} }
.toggle-key.active { .toggle-key.active {
background-color: rgb(0, 122, 255); background-color: rgb(16, 185, 129);
border-color: rgb(0, 122, 255); border-color: rgb(16, 185, 129);
color: white; color: white;
} }
.toggle-key.active:hover { .toggle-key.active:hover {
background-color: rgb(0, 100, 220); background-color: rgb(5, 150, 105);
} }
/* Ctrl shortcut button styling */ /* Ctrl shortcut button styling */

View file

@ -79,6 +79,9 @@ describe('Terminal', () => {
// Now terminal should be created // Now terminal should be created
const terminal = (element as unknown as { terminal: MockTerminal }).terminal; const terminal = (element as unknown as { terminal: MockTerminal }).terminal;
expect(terminal).toBeDefined(); expect(terminal).toBeDefined();
// Should call scrollToTop on initialization
expect(terminal.scrollToTop).toHaveBeenCalled();
}); });
it('should handle custom dimensions', async () => { it('should handle custom dimensions', async () => {

View file

@ -238,6 +238,12 @@ export class Terminal extends LitElement {
this.setupResize(); this.setupResize();
this.setupScrolling(); this.setupScrolling();
// Ensure terminal starts at the top
this.viewportY = 0;
if (this.terminal) {
this.terminal.scrollToTop();
}
this.requestUpdate(); this.requestUpdate();
} catch (error: unknown) { } catch (error: unknown) {
logger.error('failed to initialize terminal:', error); logger.error('failed to initialize terminal:', error);
@ -283,7 +289,7 @@ export class Terminal extends LitElement {
theme: { theme: {
background: '#1e1e1e', background: '#1e1e1e',
foreground: '#d4d4d4', foreground: '#d4d4d4',
cursor: '#00ff00', cursor: '#10B981',
cursorAccent: '#1e1e1e', cursorAccent: '#1e1e1e',
// Standard 16 colors (0-15) - using proper xterm colors // Standard 16 colors (0-15) - using proper xterm colors
black: '#000000', black: '#000000',
@ -938,7 +944,7 @@ export class Terminal extends LitElement {
// Apply cursor styling after inverse to ensure it takes precedence // Apply cursor styling after inverse to ensure it takes precedence
if (isCursor) { if (isCursor) {
style += `background-color: #23d18b;`; style += `background-color: #10B981;`;
} }
// Handle invisible text // Handle invisible text

View file

@ -102,7 +102,7 @@ export class VibeLogo extends LitElement {
<div class="font-mono text-sm select-none leading-tight text-center"> <div class="font-mono text-sm select-none leading-tight text-center">
<pre <pre
class="whitespace-pre" class="whitespace-pre"
>${coloredLeft} <span class="text-vs-user">VibeTunnel</span> ${coloredRight}</pre> >${coloredLeft} <span class="text-primary">VibeTunnel</span> ${coloredRight}</pre>
</div> </div>
`; `;
} }

View file

@ -40,7 +40,7 @@
/* Default focus styles */ /* Default focus styles */
:focus { :focus {
outline: none; 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 */ /* iOS Safe Area Support */
@ -65,45 +65,84 @@
@layer components { @layer components {
/* Glowing terminal icon */ /* Glowing terminal icon */
.terminal-icon { .terminal-icon {
@apply text-accent-green; @apply text-primary;
filter: drop-shadow(0 0 10px rgba(0, 255, 136, 0.6)); filter: drop-shadow(0 0 10px rgba(16, 185, 129, 0.6));
} }
/* Input fields with glow effect */ /* Input fields with unified style */
.input-field { .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 transition-all duration-200 ease-in-out;
@apply hover:border-accent-green-darker focus:border-accent-green; @apply hover:border-primary/50 focus:border-primary;
@apply focus:shadow-glow-green-sm; @apply focus:shadow-glow-sm focus:outline-none;
@apply text-center placeholder:text-center; @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 { .btn-primary {
@apply bg-accent-green text-dark-bg font-medium px-6 py-3 rounded-lg; @apply btn-md bg-primary text-black;
@apply transition-all duration-200 ease-in-out; @apply hover:bg-primary-hover hover:shadow-glow;
@apply hover:bg-accent-green-light hover:shadow-glow-green;
@apply active:scale-95; @apply active:scale-95;
} }
.btn-secondary { .btn-secondary {
@apply border border-accent-green text-accent-green px-6 py-3 rounded-lg; @apply btn-md border border-primary text-primary;
@apply transition-all duration-200 ease-in-out; @apply hover:bg-primary/10 hover:border-primary-hover hover:shadow-glow-sm;
@apply hover:bg-accent-green hover:text-dark-bg hover:shadow-glow-green;
@apply active:scale-95; @apply active:scale-95;
} }
.btn-ghost { .btn-ghost {
@apply text-dark-text-muted px-4 py-2 rounded-lg; @apply btn-md text-dark-text-muted;
@apply transition-all duration-200 ease-in-out;
@apply hover:text-dark-text hover:bg-dark-bg-tertiary; @apply hover:text-dark-text hover:bg-dark-bg-tertiary;
} }
/* Enhanced card styles */ /* Enhanced card styles */
.card { .card {
@apply bg-dark-bg-secondary border border-dark-border rounded-lg p-0; @apply bg-dark-bg-secondary border border-dark-border rounded-lg p-0;
@apply transition-all duration-200 ease-in-out shadow-sm; @apply transition-all duration-300 ease-out shadow-sm;
@apply hover:bg-dark-bg-tertiary hover:border-dark-border-light hover:shadow-card-hover; @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 { .card-elevated {
@ -118,7 +157,7 @@
} }
.status-badge-success { .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 { .status-badge-warning {
@ -137,12 +176,12 @@
.quick-start-btn { .quick-start-btn {
@apply bg-dark-bg-tertiary border border-dark-border rounded-lg px-4 py-3 h-12; @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 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; @apply active:scale-95;
} }
.quick-start-btn.active { .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 */ /* Modal backdrop */
@ -168,14 +207,15 @@
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
grid-auto-rows: 400px; grid-auto-rows: 400px;
gap: 1.25rem; gap: 1.25rem;
padding: 0 0.75rem; padding: 0.5rem 0.75rem;
/* Enable smooth grid transitions */ /* Enable smooth grid transitions */
transition: grid-template-columns 0.3s ease-out; transition: grid-template-columns 0.3s ease-out;
} }
.session-flex-responsive > * { .session-flex-responsive > * {
height: 100%; height: 100%;
overflow: hidden; /* Allow overflow for hover effects */
overflow: visible;
} }
@media (max-width: 420px) { @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 */ /* Micro-interactions and animations */
@layer utilities { @layer utilities {
/* Smooth transitions for interactive elements */ /* Smooth transitions for interactive elements */
@ -280,17 +330,21 @@
} }
/* Hover glow effect */ /* Hover glow effect */
.hover-glow-primary:hover { .hover-glow:hover {
@apply shadow-glow-primary; @apply shadow-glow;
} }
.hover-glow-green:hover { .hover-glow-sm:hover {
@apply shadow-glow-green; @apply shadow-glow-sm;
}
.hover-glow-lg:hover {
@apply shadow-glow-lg;
} }
/* Focus within styles */ /* Focus within styles */
.focus-within-glow:focus-within { .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 */ /* Smooth color transitions */
@ -714,11 +768,8 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
/* Enhanced terminal container styling */ /* Enhanced terminal container styling */
.terminal-container { .terminal-container {
color: #e4e4e4; color: #e4e4e4;
white-space: pre;
overflow: hidden; overflow: hidden;
background-color: #0a0a0a; background-color: #0a0a0a;
border-radius: 0.5rem;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5);
} }
/* Terminal line styling */ /* Terminal line styling */
@ -773,7 +824,7 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
/* Enhanced cursor styling */ /* Enhanced cursor styling */
.terminal-char.cursor { .terminal-char.cursor {
animation: cursor-blink 1s infinite; animation: cursor-blink 1s infinite;
background-color: #00D9FF; background-color: #10B981;
color: #0a0a0a; color: #0a0a0a;
} }
@ -781,12 +832,12 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
0%, 0%,
50% { 50% {
opacity: 1; opacity: 1;
background-color: #00D9FF; background-color: #10B981;
} }
51%, 51%,
100% { 100% {
opacity: 0.4; 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 */ /* Enhanced terminal focus indicator */
.terminal-focused { .terminal-focused {
box-shadow: 0 0 0 2px #00D9FF, 0 0 20px rgba(0, 217, 255, 0.3); box-shadow: 0 0 0 2px #10B981, 0 0 20px rgba(16, 185, 129, 0.3);
border-color: #00D9FF !important; border-color: #10B981 !important;
transition: all 0.2s ease-out; transition: all 0.2s ease-out;
} }
@ -814,17 +865,17 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
position: fixed; position: fixed;
top: 10px; top: 10px;
right: 10px; right: 10px;
background: rgba(0, 217, 255, 0.1); background: rgba(16, 185, 129, 0.1);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
border: 1px solid #00D9FF; border: 1px solid #10B981;
color: #00D9FF; color: #10B981;
padding: 6px 12px; padding: 6px 12px;
border-radius: 6px; border-radius: 6px;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
z-index: 1000; z-index: 1000;
animation: slideInFromTop 0.3s ease-out; 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 { @keyframes slideInFromTop {
@ -1161,8 +1212,8 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
/* Hover state for both buttons */ /* Hover state for both buttons */
.scroll-to-bottom:hover, .scroll-to-bottom:hover,
.keyboard-button:hover { .keyboard-button:hover {
@apply bg-dark-bg/40 border-accent-green text-accent-green; @apply bg-dark-bg/40 border-primary text-primary;
@apply -translate-y-0.5 shadow-glow-green-sm; @apply -translate-y-0.5 shadow-glow-sm;
backdrop-filter: blur(16px) saturate(1.8); backdrop-filter: blur(16px) saturate(1.8);
-webkit-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 { .fit-toggle:hover {
background: rgba(26, 26, 26, 1); background: rgba(26, 26, 26, 1);
border-color: #00ff88; border-color: #10B981;
color: #00ff88; color: #10B981;
transform: translateY(-1px); 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 { .fit-toggle:active {
@ -1239,9 +1290,9 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
} }
.fit-toggle.active { .fit-toggle.active {
border-color: #00ff88; border-color: #10B981;
color: #00ff88; color: #10B981;
box-shadow: 0 0 10px rgba(0, 255, 136, 0.3); box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
} }
/* View Transitions */ /* View Transitions */

View file

@ -4,7 +4,7 @@ module.exports = {
theme: { theme: {
extend: { extend: {
colors: { colors: {
// Enhanced Dark theme colors with better depth // Unified Dark theme colors with consistent depth
"dark-bg": "#0a0a0a", "dark-bg": "#0a0a0a",
"dark-bg-secondary": "#141414", "dark-bg-secondary": "#141414",
"dark-bg-tertiary": "#1f1f1f", "dark-bg-tertiary": "#1f1f1f",
@ -15,64 +15,52 @@ module.exports = {
"dark-border-light": "#3a3a3a", "dark-border-light": "#3a3a3a",
"dark-border-focus": "#4a4a4a", "dark-border-focus": "#4a4a4a",
// Enhanced Text colors // Text colors
"dark-text": "#e4e4e4", "dark-text": "#e4e4e4",
"dark-text-bright": "#ffffff", "dark-text-bright": "#ffffff",
"dark-text-muted": "#888888", "dark-text-muted": "#a3a3a3",
"dark-text-dim": "#666666", "dark-text-dim": "#737373",
// Modern accent colors - Cyan/Teal primary // Unified accent color - Vibrant teal-green
"accent-primary": "#00D9FF", "primary": "#10B981",
"accent-primary-dark": "#00B8E6", "primary-hover": "#059669",
"accent-primary-darker": "#0096CC", "primary-dark": "#047857",
"accent-primary-light": "#33E1FF", "primary-light": "#34D399",
"accent-primary-glow": "#00D9FF66", "primary-muted": "#10B98133",
"primary-glow": "#10B98166",
// Green accent colors (success/active) // Status colors
"accent-green": "#4ADE80", "status-error": "#EF4444",
"accent-green-dark": "#22C55E", "status-warning": "#F59E0B",
"accent-green-darker": "#16A34A", "status-success": "#10B981",
"accent-green-light": "#86EFAC", "status-info": "#3B82F6",
"accent-green-glow": "#4ADE8066",
// Secondary accent colors // Legacy mappings for gradual migration
"accent-purple": "#A78BFA", "accent-primary": "#10B981",
"accent-blue": "#60A5FA", "accent-primary-dark": "#059669",
"accent-amber": "#FFA726", "accent-primary-darker": "#047857",
"accent-primary-light": "#34D399",
// Enhanced Status colors "accent-primary-glow": "#10B98166",
"status-error": "#FF6B6B", "accent-green": "#10B981",
"status-warning": "#FFA726", "accent-green-dark": "#059669",
"status-success": "#4ADE80", "accent-green-darker": "#047857",
"status-info": "#60A5FA", "accent-green-light": "#34D399",
"accent-green-glow": "#10B98166",
// 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",
}, },
boxShadow: { boxShadow: {
// Updated glow effects with new colors // Unified glow effects with primary color
'glow-primary': '0 0 20px rgba(0, 217, 255, 0.4)', 'glow': '0 0 20px rgba(16, 185, 129, 0.4)',
'glow-primary-sm': '0 0 10px rgba(0, 217, 255, 0.3)', 'glow-sm': '0 0 10px rgba(16, 185, 129, 0.3)',
'glow-primary-lg': '0 0 30px rgba(0, 217, 255, 0.5)', 'glow-lg': '0 0 30px rgba(16, 185, 129, 0.5)',
'glow-green': '0 0 20px rgba(74, 222, 128, 0.4)', 'glow-intense': '0 0 40px rgba(16, 185, 129, 0.6)',
'glow-green-sm': '0 0 10px rgba(74, 222, 128, 0.3)', // Legacy mappings
'glow-green-lg': '0 0 30px rgba(74, 222, 128, 0.5)', 'glow-primary': '0 0 20px rgba(16, 185, 129, 0.4)',
// New subtle shadows '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': '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)', '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)', 'elevated': '0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2)',