mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +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.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>
|
||||||
`
|
`
|
||||||
: ''
|
: ''
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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' &&
|
||||||
|
|
|
||||||
|
|
@ -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 }));
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ||
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
`
|
`
|
||||||
: ''
|
: ''
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
`
|
`
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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"/>
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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)',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue