vibetunnel/web/src/client/components/session-view/session-header.ts

214 lines
9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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>
`;
}
}