vibetunnel/web/src/client/components/header-base.ts
Manuel Maly bc370452ad feat(web): implement vertical tabs with Arc-style persistent sidebar and comprehensive mobile UX
- Adds Arc-style persistent sidebar navigation with resizable vertical tabs
- Implements comprehensive mobile responsiveness with slide animations and hamburger menu
- Creates utility modules for responsive design, constants, and terminal management
- Refactors header components into specialized classes for better separation of concerns
- Implements ResizeObserver-based responsive design system for efficient viewport tracking
- Fixes mobile scrolling issues and eliminates layout shift bugs
- Improves session card consistency and status indicator positioning
- Adds proper terminal resize events when switching between sessions
- Enhances sidebar UX with compact headers, uniform borders, and smooth transitions
- Centralizes UI constants and breakpoints for maintainable responsive design
- Resolves TypeScript errors with web-push dependency reinstallation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-25 02:11:51 +02:00

94 lines
2.8 KiB
TypeScript

/**
* Base functionality for header components
*/
import { LitElement } from 'lit';
import { property, state } from 'lit/decorators.js';
import type { Session } from './session-list.js';
import { TIMING } from '../utils/constants.js';
export abstract class HeaderBase extends LitElement {
createRenderRoot() {
return this;
}
@property({ type: Array }) sessions: Session[] = [];
@property({ type: Boolean }) hideExited = true;
@property({ type: String }) currentUser: string | null = null;
@property({ type: String }) authMethod: string | null = null;
@state() protected killingAll = false;
@state() protected showUserMenu = false;
protected get runningSessions(): Session[] {
return this.sessions.filter((session) => session.status === 'running');
}
protected get exitedSessions(): Session[] {
return this.sessions.filter((session) => session.status === 'exited');
}
protected handleCreateSession(e: MouseEvent) {
// Capture button position for view transition
const button = e.currentTarget as HTMLButtonElement;
const rect = button.getBoundingClientRect();
// Store position in CSS custom properties for the transition
document.documentElement.style.setProperty('--vt-button-x', `${rect.left + rect.width / 2}px`);
document.documentElement.style.setProperty('--vt-button-y', `${rect.top + rect.height / 2}px`);
document.documentElement.style.setProperty('--vt-button-width', `${rect.width}px`);
document.documentElement.style.setProperty('--vt-button-height', `${rect.height}px`);
this.dispatchEvent(new CustomEvent('create-session'));
}
protected handleKillAll() {
if (this.killingAll) return;
this.killingAll = true;
this.requestUpdate();
this.dispatchEvent(new CustomEvent('kill-all-sessions'));
// Reset after a delay to prevent multiple clicks
window.setTimeout(() => {
this.killingAll = false;
}, TIMING.KILL_ALL_BUTTON_DISABLE_DURATION);
}
protected handleCleanExited() {
this.dispatchEvent(new CustomEvent('clean-exited-sessions'));
}
protected handleHideExitedToggle() {
this.dispatchEvent(
new CustomEvent('hide-exited-change', {
detail: !this.hideExited,
})
);
}
protected handleLogout() {
this.showUserMenu = false;
this.dispatchEvent(new CustomEvent('logout'));
}
protected toggleUserMenu() {
this.showUserMenu = !this.showUserMenu;
}
protected handleClickOutside = (e: Event) => {
const target = e.target as HTMLElement;
if (!target.closest('.user-menu-container')) {
this.showUserMenu = false;
}
};
connectedCallback() {
super.connectedCallback();
document.addEventListener('click', this.handleClickOutside);
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('click', this.handleClickOutside);
}
}