mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-15 12:55:52 +00:00
214 lines
9 KiB
TypeScript
214 lines
9 KiB
TypeScript
/**
|
||
* Session Header Component
|
||
*
|
||
* Header bar for session view with navigation, session info, status, and controls.
|
||
* Includes back button, sidebar toggle, session details, and terminal controls.
|
||
*/
|
||
import { html, LitElement } from 'lit';
|
||
import { customElement, property } from 'lit/decorators.js';
|
||
import type { Session } from '../session-list.js';
|
||
import '../clickable-path.js';
|
||
import './width-selector.js';
|
||
|
||
@customElement('session-header')
|
||
export class SessionHeader extends LitElement {
|
||
// Disable shadow DOM to use Tailwind
|
||
createRenderRoot() {
|
||
return this;
|
||
}
|
||
|
||
@property({ type: Object }) session: Session | null = null;
|
||
@property({ type: Boolean }) showBackButton = true;
|
||
@property({ type: Boolean }) showSidebarToggle = false;
|
||
@property({ type: Boolean }) sidebarCollapsed = false;
|
||
@property({ type: Number }) terminalCols = 0;
|
||
@property({ type: Number }) terminalRows = 0;
|
||
@property({ type: Number }) terminalMaxCols = 0;
|
||
@property({ type: Number }) terminalFontSize = 14;
|
||
@property({ type: String }) customWidth = '';
|
||
@property({ type: Boolean }) showWidthSelector = false;
|
||
@property({ type: String }) widthLabel = '';
|
||
@property({ type: String }) widthTooltip = '';
|
||
@property({ type: Function }) onBack?: () => void;
|
||
@property({ type: Function }) onSidebarToggle?: () => void;
|
||
@property({ type: Function }) onOpenFileBrowser?: () => void;
|
||
@property({ type: Function }) onCreateSession?: () => void;
|
||
@property({ type: Function }) onOpenImagePicker?: () => void;
|
||
@property({ type: Function }) onMaxWidthToggle?: () => void;
|
||
@property({ type: Function }) onWidthSelect?: (width: number) => void;
|
||
@property({ type: Function }) onFontSizeChange?: (size: number) => void;
|
||
|
||
private getStatusText(): string {
|
||
if (!this.session) return '';
|
||
if ('active' in this.session && this.session.active === false) {
|
||
return 'waiting';
|
||
}
|
||
return this.session.status;
|
||
}
|
||
|
||
private getStatusColor(): string {
|
||
if (!this.session) return 'text-dark-text-muted';
|
||
if ('active' in this.session && this.session.active === false) {
|
||
return 'text-dark-text-muted';
|
||
}
|
||
return this.session.status === 'running' ? 'text-status-success' : 'text-status-warning';
|
||
}
|
||
|
||
private getStatusDotColor(): string {
|
||
if (!this.session) return 'bg-dark-text-muted';
|
||
if ('active' in this.session && this.session.active === false) {
|
||
return 'bg-dark-text-muted';
|
||
}
|
||
return this.session.status === 'running' ? 'bg-status-success' : 'bg-status-warning';
|
||
}
|
||
|
||
private handleCloseWidthSelector() {
|
||
this.dispatchEvent(
|
||
new CustomEvent('close-width-selector', {
|
||
bubbles: true,
|
||
composed: true,
|
||
})
|
||
);
|
||
}
|
||
|
||
render() {
|
||
if (!this.session) return null;
|
||
|
||
return html`
|
||
<!-- Compact Header -->
|
||
<div
|
||
class="flex items-center justify-between border-b border-dark-border text-sm min-w-0 bg-dark-bg-secondary p-3"
|
||
style="padding-top: max(0.75rem, calc(0.75rem + env(safe-area-inset-top))); padding-left: max(0.75rem, env(safe-area-inset-left)); padding-right: max(0.75rem, env(safe-area-inset-right));"
|
||
>
|
||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||
<!-- Sidebar Toggle and Create Session Buttons (shown when sidebar is collapsed) -->
|
||
${
|
||
this.showSidebarToggle && this.sidebarCollapsed
|
||
? html`
|
||
<div class="flex items-center gap-2">
|
||
<button
|
||
class="bg-dark-bg-tertiary border border-dark-border rounded-lg p-1.5 font-mono text-dark-text-muted transition-all duration-300 hover:text-dark-text hover:bg-dark-bg hover:border-accent-green flex-shrink-0"
|
||
@click=${() => this.onSidebarToggle?.()}
|
||
title="Show sidebar (⌘B)"
|
||
aria-label="Show sidebar"
|
||
aria-expanded="false"
|
||
aria-controls="sidebar"
|
||
>
|
||
<!-- Right chevron icon to expand sidebar -->
|
||
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
|
||
<path d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"/>
|
||
</svg>
|
||
</button>
|
||
|
||
<!-- Create Session button -->
|
||
<button
|
||
class="bg-dark-bg-tertiary border border-accent-green text-accent-green rounded-lg p-1.5 font-mono transition-all duration-300 hover:bg-accent-green hover:text-dark-bg flex-shrink-0"
|
||
@click=${() => this.onCreateSession?.()}
|
||
title="Create New Session (⌘K)"
|
||
>
|
||
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
|
||
<path d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
`
|
||
: ''
|
||
}
|
||
${
|
||
this.showBackButton
|
||
? html`
|
||
<button
|
||
class="btn-secondary font-mono text-xs px-3 py-1 flex-shrink-0"
|
||
@click=${() => this.onBack?.()}
|
||
>
|
||
Back
|
||
</button>
|
||
`
|
||
: ''
|
||
}
|
||
<div class="text-dark-text min-w-0 flex-1 overflow-hidden max-w-[50vw] sm:max-w-none">
|
||
<div
|
||
class="text-accent-green text-xs sm:text-sm overflow-hidden text-ellipsis whitespace-nowrap"
|
||
title="${
|
||
this.session.name ||
|
||
(Array.isArray(this.session.command)
|
||
? this.session.command.join(' ')
|
||
: this.session.command)
|
||
}"
|
||
>
|
||
${
|
||
this.session.name ||
|
||
(Array.isArray(this.session.command)
|
||
? this.session.command.join(' ')
|
||
: this.session.command)
|
||
}
|
||
</div>
|
||
<div class="text-xs opacity-75 mt-0.5 overflow-hidden">
|
||
<clickable-path
|
||
.path=${this.session.workingDir}
|
||
.iconSize=${12}
|
||
></clickable-path>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-2 text-xs flex-shrink-0 ml-2 relative">
|
||
<button
|
||
class="btn-secondary font-mono text-xs p-1 flex-shrink-0"
|
||
@click=${() => this.onOpenFileBrowser?.()}
|
||
title="Browse Files (⌘O)"
|
||
>
|
||
<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
|
||
class="btn-secondary font-mono text-xs p-1 flex-shrink-0"
|
||
@click=${() => this.onOpenImagePicker?.()}
|
||
title="Upload File"
|
||
>
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z" stroke="currentColor" stroke-width="2"/>
|
||
<path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="2"/>
|
||
</svg>
|
||
</button>
|
||
<button
|
||
class="btn-secondary font-mono text-xs px-2 py-1 flex-shrink-0 width-selector-button"
|
||
@click=${() => this.onMaxWidthToggle?.()}
|
||
title="${this.widthTooltip}"
|
||
>
|
||
${this.widthLabel}
|
||
</button>
|
||
<width-selector
|
||
.visible=${this.showWidthSelector}
|
||
.terminalMaxCols=${this.terminalMaxCols}
|
||
.terminalFontSize=${this.terminalFontSize}
|
||
.customWidth=${this.customWidth}
|
||
.onWidthSelect=${(width: number) => this.onWidthSelect?.(width)}
|
||
.onFontSizeChange=${(size: number) => this.onFontSizeChange?.(size)}
|
||
.onClose=${() => this.handleCloseWidthSelector()}
|
||
></width-selector>
|
||
<div class="flex flex-col items-end gap-0">
|
||
<span class="${this.getStatusColor()} text-xs flex items-center gap-1">
|
||
<div class="w-2 h-2 rounded-full ${this.getStatusDotColor()}"></div>
|
||
${this.getStatusText().toUpperCase()}
|
||
</span>
|
||
${
|
||
this.terminalCols > 0 && this.terminalRows > 0
|
||
? html`
|
||
<span
|
||
class="text-dark-text-muted text-xs opacity-60"
|
||
style="font-size: 10px; line-height: 1;"
|
||
>
|
||
${this.terminalCols}×${this.terminalRows}
|
||
</span>
|
||
`
|
||
: ''
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|