Add professional light mode with theme toggle (#314)

This commit is contained in:
Peter Steinberger 2025-07-12 01:42:42 +02:00 committed by GitHub
parent 0a50ccc65e
commit e3d0d9655b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 1373 additions and 875 deletions

View file

@ -1,8 +1,8 @@
// VibeTunnel Version Configuration
// This file contains the version and build number for the app
MARKETING_VERSION = 1.0.0-beta.9
CURRENT_PROJECT_VERSION = 173
MARKETING_VERSION = 1.0.0-beta.10
CURRENT_PROJECT_VERSION = 180
// Domain and GitHub configuration
APP_DOMAIN = vibetunnel.sh

View file

@ -1,6 +1,6 @@
{
"name": "@vibetunnel/vibetunnel-cli",
"version": "1.0.0-beta.9",
"version": "1.0.0-beta.10",
"description": "Web frontend for terminal multiplexer",
"main": "dist/server.js",
"bin": {

View file

@ -496,15 +496,9 @@ export class VibeTunnelApp extends LitElement {
return;
}
// Add class to prevent flicker when closing modal
document.body.classList.add('modal-closing');
// Simply close the modal without animation
this.showCreateModal = false;
// Remove the class after a short delay
setTimeout(() => {
document.body.classList.remove('modal-closing');
}, 300);
// Check if this was a terminal spawn (not a web session)
if (message?.includes('Terminal spawned successfully')) {
// Don't try to switch to the session - it's running in a terminal window
@ -684,40 +678,9 @@ export class VibeTunnelApp extends LitElement {
}
private handleCreateModalClose() {
// Immediately hide the modal
// Simply close the modal without animation
this.showCreateModal = false;
// Skip animation if we're in session detail view
const isInSessionDetailView = this.currentView === 'session';
// Then apply view transition if supported (non-blocking)
if (
!isInSessionDetailView &&
'startViewTransition' in document &&
typeof document.startViewTransition === 'function'
) {
// Add a class to prevent flicker during transition
document.body.classList.add('modal-closing');
// Set data attribute to indicate transition is starting
document.documentElement.setAttribute('data-view-transition', 'active');
try {
const transition = document.startViewTransition(() => {
// Force a re-render
this.requestUpdate();
});
// Clean up the class and attribute after transition
transition.finished.finally(() => {
document.body.classList.remove('modal-closing');
document.documentElement.removeAttribute('data-view-transition');
});
} catch (_error) {
// If view transition fails, clean up
document.body.classList.remove('modal-closing');
document.documentElement.removeAttribute('data-view-transition');
}
}
this.requestUpdate();
}
private cleanupSessionViewStream(): void {
@ -1236,7 +1199,7 @@ export class VibeTunnelApp extends LitElement {
return 'w-full min-h-screen flex flex-col';
}
const baseClasses = 'bg-dark-bg-secondary border-r border-dark-border flex flex-col';
const baseClasses = 'bg-secondary border-r border-base flex flex-col';
const isMobile = this.mediaState.isMobile;
// Only apply transition class when animations are ready (not during initial load)
const transitionClass = this.sidebarAnimationReady && !isMobile ? 'sidebar-transition' : '';
@ -1359,7 +1322,7 @@ export class VibeTunnelApp extends LitElement {
? html`
<div class="fixed top-4 right-4" style="z-index: ${Z_INDEX.MODAL_BACKDROP};">
<div
class="bg-status-error text-dark-bg px-4 py-2 rounded shadow-lg font-mono text-sm"
class="bg-status-error text-bg-elevated px-4 py-2 rounded shadow-lg font-mono text-sm"
>
${this.errorMessage}
<button
@ -1370,7 +1333,7 @@ export class VibeTunnelApp extends LitElement {
}
this.errorMessage = '';
}}
class="ml-2 text-dark-bg hover:text-dark-text"
class="ml-2 text-bg-elevated hover:text-text-muted"
>
</button>
@ -1384,7 +1347,7 @@ export class VibeTunnelApp extends LitElement {
? html`
<div class="fixed top-4 right-4" style="z-index: ${Z_INDEX.MODAL_BACKDROP};">
<div
class="bg-status-success text-dark-bg px-4 py-2 rounded shadow-lg font-mono text-sm"
class="bg-status-success text-bg-elevated px-4 py-2 rounded shadow-lg font-mono text-sm"
>
${this.successMessage}
<button
@ -1395,7 +1358,7 @@ export class VibeTunnelApp extends LitElement {
}
this.successMessage = '';
}}
class="ml-2 text-dark-bg hover:text-dark-text"
class="ml-2 text-bg-elevated hover:text-text-muted"
>
</button>
@ -1437,7 +1400,7 @@ export class VibeTunnelApp extends LitElement {
<div
class="fixed inset-0 sm:hidden transition-all ${
this.isInSidebarDismissMode
? 'bg-black bg-opacity-50 backdrop-blur-sm'
? 'bg-bg bg-opacity-50 backdrop-blur-sm'
: 'bg-transparent pointer-events-none'
}"
style="z-index: ${Z_INDEX.MOBILE_OVERLAY}; transition-duration: ${TRANSITIONS.MOBILE_SLIDE}ms;"
@ -1465,7 +1428,7 @@ export class VibeTunnelApp extends LitElement {
@navigate-to-list=${this.handleNavigateToList}
@toggle-sidebar=${this.handleToggleSidebar}
></app-header>
<div class="${this.showSplitView ? 'flex-1 overflow-y-auto' : 'flex-1'} bg-dark-bg-secondary">
<div class="${this.showSplitView ? 'flex-1 overflow-y-auto' : 'flex-1'} bg-secondary">
<session-list
.sessions=${this.sessions}
.loading=${this.loading}
@ -1490,7 +1453,7 @@ export class VibeTunnelApp extends LitElement {
this.shouldShowResizeHandle
? html`
<div
class="w-1 bg-dark-border hover:bg-accent-green cursor-ew-resize transition-colors ${
class="w-1 bg-border hover:bg-accent-green cursor-ew-resize transition-colors ${
this.isResizing ? 'bg-accent-green' : ''
}"
style="transition-duration: ${TRANSITIONS.RESIZE_HANDLE}ms;"
@ -1562,8 +1525,8 @@ export class VibeTunnelApp extends LitElement {
${
this.showLogLink
? html`
<div class="fixed ${this.getLogButtonPosition()} right-4 text-dark-text-muted text-xs font-mono bg-dark-bg-secondary px-3 py-1.5 rounded-lg border border-dark-border shadow-sm transition-all duration-200" style="z-index: ${Z_INDEX.LOG_BUTTON};">
<a href="/logs" class="hover:text-dark-text transition-colors">Logs</a>
<div class="fixed ${this.getLogButtonPosition()} right-4 text-muted text-xs font-mono bg-secondary px-3 py-1.5 rounded-lg border border-base shadow-sm transition-all duration-200" style="z-index: ${Z_INDEX.LOG_BUTTON};">
<a href="/logs" class="hover:text-text transition-colors">Logs</a>
<span class="ml-2 opacity-75">v${VERSION}</span>
</div>
`

View file

@ -73,8 +73,33 @@
}
}
</script>
<!-- Initialize theme immediately to prevent flash -->
<script>
(function() {
// Load saved theme preference or use system preference
const saved = localStorage.getItem('vibetunnel-theme');
const theme = saved || 'system';
// Apply theme immediately
if (theme === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
// Apply immediate background to prevent flash
const effectiveTheme = theme === 'system'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: theme;
// Set initial background color
document.documentElement.style.backgroundColor = effectiveTheme === 'dark' ? '#0a0a0a' : '#fafafa';
})();
</script>
</head>
<body class="m-0 p-0" style="background: black">
<body class="m-0 p-0 bg-bg text-text">
<vibetunnel-app></vibetunnel-app>
<!-- Mobile viewport height fix -->

View file

@ -39,8 +39,33 @@
height: 100vh;
}
</style>
<!-- Initialize theme immediately to prevent flash -->
<script>
(function() {
// Load saved theme preference or use system preference
const saved = localStorage.getItem('vibetunnel-theme');
const theme = saved || 'system';
// Apply theme immediately
if (theme === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
} else {
document.documentElement.setAttribute('data-theme', theme);
}
// Apply immediate background to prevent flash
const effectiveTheme = theme === 'system'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: theme;
// Set initial background color
document.documentElement.style.backgroundColor = effectiveTheme === 'dark' ? '#0a0a0a' : '#fafafa';
})();
</script>
</head>
<body class="m-0 p-0" style="background: black">
<body class="m-0 p-0 bg-bg text-text">
<log-viewer></log-viewer>
<script type="module" src="bundle/client-bundle.js"></script>

View file

@ -162,7 +162,7 @@ export class AuthLogin extends LitElement {
<div class="auth-container">
<!-- Settings button in top right corner -->
<button
class="absolute top-4 right-4 p-2 text-dark-text-muted hover:text-dark-text transition-colors"
class="absolute top-4 right-4 p-2 text-muted hover:text-primary transition-colors"
@click=${this.handleOpenSettings}
title="Settings"
>
@ -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">
<terminal-icon
size="${this.isMobile ? '48' : '56'}"
style="filter: drop-shadow(0 0 15px rgba(16, 185, 129, 0.4));"
style="filter: drop-shadow(0 0 15px rgb(var(--color-primary) / 0.4));"
></terminal-icon>
<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>
@ -187,7 +187,7 @@ export class AuthLogin extends LitElement {
this.error
? html`
<div
class="bg-status-error text-dark-bg px-3 py-1.5 rounded mb-3 font-mono text-xs sm:text-sm"
class="bg-status-error text-base px-3 py-1.5 rounded mb-3 font-mono text-xs sm:text-sm"
data-testid="error-message"
>
${this.error}
@ -195,7 +195,7 @@ export class AuthLogin extends LitElement {
@click=${() => {
this.error = '';
}}
class="ml-2 text-dark-bg hover:text-dark-text"
class="ml-2 text-base hover:text-primary"
data-testid="error-close"
>
@ -208,14 +208,14 @@ export class AuthLogin extends LitElement {
this.success
? html`
<div
class="bg-status-success text-dark-bg px-3 py-1.5 rounded mb-3 font-mono text-xs sm:text-sm"
class="bg-status-success text-base px-3 py-1.5 rounded mb-3 font-mono text-xs sm:text-sm"
>
${this.success}
<button
@click=${() => {
this.success = '';
}}
class="ml-2 text-dark-bg hover:text-dark-text"
class="ml-2 text-base hover:text-primary"
>
</button>
@ -233,7 +233,7 @@ export class AuthLogin extends LitElement {
<div class="flex flex-col items-center mb-4 sm:mb-6">
<div
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(16, 185, 129, 0.3);"
style="box-shadow: 0 0 25px rgb(var(--color-primary) / 0.3);"
>
${
this.userAvatar
@ -248,10 +248,10 @@ export class AuthLogin extends LitElement {
`
: html`
<div
class="w-full h-full bg-dark-bg-secondary flex items-center justify-center"
class="w-full h-full bg-secondary flex items-center justify-center"
>
<svg
class="w-12 h-12 sm:w-14 sm:h-14 text-dark-text-muted"
class="w-12 h-12 sm:w-14 sm:h-14 text-muted"
fill="currentColor"
viewBox="0 0 20 20"
>
@ -261,7 +261,7 @@ export class AuthLogin extends LitElement {
`
}
</div>
<p class="text-dark-text text-base sm:text-lg font-medium">
<p class="text-primary text-base sm:text-lg font-medium">
Welcome back, ${this.currentUserId || '...'}
</p>
</div>
@ -300,7 +300,7 @@ export class AuthLogin extends LitElement {
<div class="ssh-key-item p-6 sm:p-8">
<div class="flex flex-col items-center mb-4 sm:mb-6">
<div
class="w-16 h-16 sm:w-20 sm:h-20 rounded-full mb-2 sm:mb-3 overflow-hidden border-2 border-dark-border"
class="w-16 h-16 sm:w-20 sm:h-20 rounded-full mb-2 sm:mb-3 overflow-hidden border-2 border-base"
>
${
this.userAvatar
@ -315,10 +315,10 @@ export class AuthLogin extends LitElement {
`
: html`
<div
class="w-full h-full bg-dark-bg-secondary flex items-center justify-center"
class="w-full h-full bg-secondary flex items-center justify-center"
>
<svg
class="w-8 h-8 sm:w-10 sm:h-10 text-dark-text-muted"
class="w-8 h-8 sm:w-10 sm:h-10 text-muted"
fill="currentColor"
viewBox="0 0 20 20"
>
@ -328,14 +328,14 @@ export class AuthLogin extends LitElement {
`
}
</div>
<p class="text-dark-text text-xs sm:text-sm">
<p class="text-primary text-xs sm:text-sm">
${
this.currentUserId
? `Welcome back, ${this.currentUserId}`
: 'Please authenticate to continue'
}
</p>
<p class="text-dark-text-muted text-xs mt-1 sm:mt-2">
<p class="text-muted text-xs mt-1 sm:mt-2">
SSH key authentication required
</p>
</div>
@ -374,11 +374,11 @@ export class AuthLogin extends LitElement {
</div>
<div class="space-y-3">
<div class="bg-dark-bg border border-dark-border rounded p-3">
<p class="text-dark-text-muted text-xs mb-2">
<div class="bg-base border border-base rounded p-3">
<p class="text-muted text-xs mb-2">
Generate SSH keys for browser-based authentication
</p>
<p class="text-dark-text-muted text-xs">
<p class="text-muted text-xs">
💡 SSH keys work in both browser and terminal
</p>
</div>

View file

@ -33,13 +33,13 @@ export class AuthQuickKeys extends LitElement {
render() {
return html`
<div class="quick-keys-bar bg-dark-bg-secondary border-t border-dark-border p-2">
<div class="quick-keys-bar bg-secondary border-t border-base p-2">
<div class="flex gap-1 overflow-x-auto scrollbar-hide">
${QUICK_KEYS.map(
({ key, label }) => html`
<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-primary transition-all whitespace-nowrap flex-shrink-0"
class="quick-key-btn px-3 py-1.5 bg-tertiary text-primary text-xs font-mono rounded border border-base hover:bg-surface hover:border-primary transition-all whitespace-nowrap flex-shrink-0"
@click=${() => this.handleKeyPress(key)}
>
${label}

View file

@ -15,26 +15,26 @@ export class FileBrowserFAB extends LitElement {
width: 56px;
height: 56px;
border-radius: 50%;
background: #007acc;
color: white;
background: rgb(var(--color-primary));
color: rgb(var(--color-text-bright));
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
box-shadow: 0 4px 8px rgb(var(--color-bg-base) / 0.3);
transition: all 0.3s ease;
}
.fab:hover {
background: #005a9e;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
background: rgb(var(--color-primary-hover));
box-shadow: 0 6px 12px rgb(var(--color-bg-base) / 0.4);
transform: translateY(-2px);
}
.fab:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
box-shadow: 0 2px 4px rgb(var(--color-bg-base) / 0.3);
}
.icon {
@ -46,8 +46,8 @@ export class FileBrowserFAB extends LitElement {
bottom: 100%;
right: 0;
margin-bottom: 8px;
background: #333;
color: white;
background: rgb(var(--color-surface));
color: rgb(var(--color-text));
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;

View file

@ -392,7 +392,7 @@ export class FileBrowser extends LitElement {
private renderPreview() {
if (this.previewLoading) {
return html`
<div class="flex items-center justify-center h-full text-dark-text-muted">
<div class="flex items-center justify-center h-full text-muted">
Loading preview...
</div>
`;
@ -404,7 +404,7 @@ export class FileBrowser extends LitElement {
if (!this.preview) {
return html`
<div class="flex flex-col items-center justify-center h-full text-dark-text-muted">
<div class="flex flex-col items-center justify-center h-full text-muted">
${UIIcons.preview}
<div>Select a file to preview</div>
</div>
@ -438,11 +438,11 @@ export class FileBrowser extends LitElement {
case 'binary':
return html`
<div class="flex flex-col items-center justify-center h-full text-dark-text-muted">
<div class="flex flex-col items-center justify-center h-full text-muted">
${UIIcons.binary}
<div class="text-lg mb-2">Binary File</div>
<div class="text-sm">${this.preview.humanSize || `${this.preview.size} bytes`}</div>
<div class="text-sm text-dark-text-muted mt-2">
<div class="text-sm text-muted mt-2">
${this.preview.mimeType || 'Unknown type'}
</div>
</div>
@ -454,7 +454,7 @@ export class FileBrowser extends LitElement {
// For new files (added or untracked), we might not have a diff but we have diffContent
if (!this.diffContent && (!this.diff || !this.diff.diff)) {
return html`
<div class="flex items-center justify-center h-full text-dark-text-muted">
<div class="flex items-center justify-center h-full text-muted">
No changes in this file
</div>
`;
@ -483,9 +483,9 @@ export class FileBrowser extends LitElement {
return html`
<div class="overflow-auto h-full p-4 font-mono text-xs">
${lines.map((line) => {
let className = 'text-dark-text-muted';
if (line.startsWith('+')) className = 'text-status-success bg-green-900/20';
else if (line.startsWith('-')) className = 'text-status-error bg-red-900/20';
let className = 'text-muted';
if (line.startsWith('+')) className = 'text-status-success bg-status-success/10';
else if (line.startsWith('-')) className = 'text-status-error bg-status-error/10';
else if (line.startsWith('@@')) className = 'text-status-info font-semibold';
return html`<div class="whitespace-pre ${className}">${line}</div>`;
@ -500,12 +500,12 @@ export class FileBrowser extends LitElement {
}
return html`
<div class="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center" style="z-index: ${Z_INDEX.FILE_BROWSER};" @click=${this.handleCancel}>
<div class="fixed inset-0 bg-dark-bg flex flex-col" style="z-index: ${Z_INDEX.FILE_BROWSER};" @click=${(e: Event) => e.stopPropagation()}>
<div class="fixed inset-0 bg-bg/80 backdrop-blur-sm flex items-center justify-center" style="z-index: ${Z_INDEX.FILE_BROWSER};" @click=${this.handleCancel}>
<div class="fixed inset-0 bg-bg flex flex-col" style="z-index: ${Z_INDEX.FILE_BROWSER};" @click=${(e: Event) => e.stopPropagation()}>
${
this.isMobile && this.mobileView === 'preview'
? html`
<div class="absolute top-1/2 left-2 -translate-y-1/2 text-dark-text-muted opacity-50">
<div class="absolute top-1/2 left-2 -translate-y-1/2 text-muted opacity-50">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
@ -524,12 +524,12 @@ export class FileBrowser extends LitElement {
>
<!-- Compact Header (like session-view) -->
<div
class="flex items-center justify-between px-3 py-2 border-b border-dark-border text-sm min-w-0 bg-dark-bg-secondary"
class="flex items-center justify-between px-3 py-2 border-b border-base text-sm min-w-0 bg-secondary"
style="padding-top: max(0.5rem, 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">
<button
class="text-dark-text-muted hover:text-dark-text font-mono text-xs px-2 py-1 flex-shrink-0 transition-colors flex items-center gap-1"
class="text-muted hover:text-primary font-mono text-xs px-2 py-1 flex-shrink-0 transition-colors flex items-center gap-1"
@click=${this.handleCancel}
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -542,7 +542,7 @@ export class FileBrowser extends LitElement {
</svg>
<span>Back</span>
</button>
<div class="text-dark-text min-w-0 flex-1 overflow-hidden">
<div class="text-primary min-w-0 flex-1 overflow-hidden">
${
this.editingPath
? html`
@ -553,13 +553,13 @@ export class FileBrowser extends LitElement {
@input=${this.handlePathInput}
@keydown=${this.handlePathKeyDown}
@blur=${this.handlePathBlur}
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"
class="bg-bg border border-base 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"
/>
`
: html`
<div
class="text-blue-400 text-xs sm:text-sm overflow-hidden text-ellipsis whitespace-nowrap font-mono cursor-pointer hover:bg-dark-bg-lighter rounded px-1 py-1 -mx-1"
class="text-status-info text-xs sm:text-sm overflow-hidden text-ellipsis whitespace-nowrap font-mono cursor-pointer hover:bg-light rounded px-1 py-1 -mx-1"
title="${
this.currentFullPath || this.currentPath || 'File Browser'
} (click to edit)"
@ -576,7 +576,7 @@ export class FileBrowser extends LitElement {
this.errorMessage
? html`
<div
class="bg-red-500/20 border border-red-500 text-red-400 px-2 py-1 rounded text-xs"
class="bg-status-error/20 border border-status-error text-status-error px-2 py-1 rounded text-xs"
>
${this.errorMessage}
</div>
@ -592,16 +592,16 @@ export class FileBrowser extends LitElement {
<div
class="${this.isMobile && this.mobileView === 'preview' ? 'hidden' : ''} ${
this.isMobile ? 'w-full' : 'w-80'
} bg-dark-bg-secondary border-r border-dark-border flex flex-col"
} bg-secondary border-r border-base flex flex-col"
>
<!-- File list header with toggles -->
<div
class="bg-dark-bg-secondary border-b border-dark-border p-3 flex items-center justify-between"
class="bg-secondary border-b border-base p-3 flex items-center justify-between"
>
<div class="flex gap-2">
<button
class="btn-secondary text-xs px-2 py-1 font-mono ${
this.gitFilter === 'changed' ? 'bg-primary text-black' : ''
this.gitFilter === 'changed' ? 'bg-primary text-text-bright' : ''
}"
@click=${this.toggleGitFilter}
title="Show only Git changes"
@ -610,7 +610,7 @@ export class FileBrowser extends LitElement {
</button>
<button
class="btn-secondary text-xs px-2 py-1 font-mono ${
this.showHidden ? 'bg-primary text-black' : ''
this.showHidden ? 'bg-primary text-text-bright' : ''
}"
@click=${this.toggleHidden}
title="Show hidden files"
@ -621,7 +621,7 @@ export class FileBrowser extends LitElement {
${
this.gitStatus?.branch
? html`
<span class="text-dark-text-muted text-xs flex items-center gap-1 font-mono">
<span class="text-muted text-xs flex items-center gap-1 font-mono">
${UIIcons.git} ${this.gitStatus.branch}
</span>
`
@ -636,7 +636,7 @@ export class FileBrowser extends LitElement {
${
this.loading
? html`
<div class="flex items-center justify-center h-full text-dark-text-muted">
<div class="flex items-center justify-center h-full text-muted">
Loading...
</div>
`
@ -645,11 +645,11 @@ export class FileBrowser extends LitElement {
this.currentFullPath !== '/'
? html`
<div
class="p-3 hover:bg-dark-bg-lighter cursor-pointer transition-colors flex items-center gap-2 border-b border-dark-border"
class="p-3 hover:bg-light cursor-pointer transition-colors flex items-center gap-2 border-b border-base"
@click=${this.handleParentClick}
>
${getParentDirectoryIcon()}
<span class="text-dark-text-muted">..</span>
<span class="text-muted">..</span>
</div>
`
: ''
@ -657,10 +657,10 @@ export class FileBrowser extends LitElement {
${this.files.map(
(file) => html`
<div
class="p-3 hover:bg-dark-bg-lighter cursor-pointer transition-colors flex items-center gap-2
class="p-3 hover:bg-light cursor-pointer transition-colors flex items-center gap-2
${
this.selectedFile?.path === file.path
? 'bg-dark-bg-lighter border-l-2 border-primary'
? 'bg-light border-l-2 border-primary'
: ''
}"
@click=${() => this.handleFileClick(file)}
@ -671,7 +671,7 @@ export class FileBrowser extends LitElement {
file.isSymlink
? html`
<svg
class="w-3 h-3 text-dark-text-muted absolute -bottom-1 -right-1"
class="w-3 h-3 text-muted absolute -bottom-1 -right-1"
fill="currentColor"
viewBox="0 0 20 20"
>
@ -687,7 +687,7 @@ export class FileBrowser extends LitElement {
</span>
<span
class="flex-1 text-sm whitespace-nowrap ${
file.type === 'directory' ? 'text-status-info' : 'text-dark-text'
file.type === 'directory' ? 'text-status-info' : 'text-text'
}"
title="${file.name}${file.isSymlink ? ' (symlink)' : ''}"
>${file.name}</span
@ -707,13 +707,13 @@ export class FileBrowser extends LitElement {
<div
class="${this.isMobile && this.mobileView === 'list' ? 'hidden' : ''} ${
this.isMobile ? 'w-full' : 'flex-1'
} bg-dark-bg flex flex-col overflow-hidden"
} bg-bg flex flex-col overflow-hidden"
>
${
this.selectedFile
? html`
<div
class="bg-dark-bg-secondary border-b border-dark-border p-3 ${
class="bg-secondary border-b border-base p-3 ${
this.isMobile ? 'space-y-2' : 'flex items-center justify-between'
}"
>
@ -725,7 +725,7 @@ export class FileBrowser extends LitElement {
@click=${() => {
this.mobileView = 'list';
}}
class="text-dark-text-muted hover:text-dark-text transition-colors flex-shrink-0"
class="text-muted hover:text-primary transition-colors flex-shrink-0"
title="Back to files"
>
<svg
@ -751,7 +751,7 @@ export class FileBrowser extends LitElement {
this.selectedFile.isSymlink
? html`
<svg
class="w-3 h-3 text-dark-text-muted absolute -bottom-1 -right-1"
class="w-3 h-3 text-muted absolute -bottom-1 -right-1"
fill="currentColor"
viewBox="0 0 20 20"
>
@ -808,7 +808,7 @@ export class FileBrowser extends LitElement {
? html`
<button
class="btn-secondary text-xs px-2 py-1 font-mono ${
this.showDiff ? 'bg-primary text-black' : ''
this.showDiff ? 'bg-primary text-text-bright' : ''
} ${
this.isMobile &&
this.selectedFile.type === 'file' &&
@ -835,7 +835,7 @@ export class FileBrowser extends LitElement {
${
this.mode === 'select'
? html`
<div class="p-4 border-t border-dark-border flex gap-4">
<div class="p-4 border-t border-base flex gap-4">
<button class="btn-ghost font-mono flex-1" @click=${this.handleCancel}>
Cancel
</button>

View file

@ -116,7 +116,7 @@ describe('FilePicker Component', () => {
const cancelEventSpy = vi.fn();
element.addEventListener('file-cancel', cancelEventSpy);
const modal = element.querySelector('.bg-dark-bg-elevated');
const modal = element.querySelector('.bg-elevated');
expect(modal).toBeTruthy();
modal?.dispatchEvent(new MouseEvent('click', { bubbles: true }));

View file

@ -216,9 +216,9 @@ export class FilePicker extends LitElement {
}
return html`
<div class="fixed inset-0 bg-black bg-opacity-80 backdrop-blur-sm flex items-center justify-center animate-fade-in" style="z-index: ${Z_INDEX.FILE_PICKER};" @click=${this.handleCancel}>
<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-xl font-bold text-dark-text mb-6">
<div class="fixed inset-0 bg-bg bg-opacity-80 backdrop-blur-sm flex items-center justify-center animate-fade-in" style="z-index: ${Z_INDEX.FILE_PICKER};" @click=${this.handleCancel}>
<div class="bg-elevated border border-base rounded-xl shadow-2xl p-8 m-4 max-w-sm w-full animate-scale-in" @click=${(e: Event) => e.stopPropagation()}>
<h3 class="text-xl font-bold text-primary mb-6">
Select File
</h3>
@ -227,12 +227,12 @@ export class FilePicker extends LitElement {
? html`
<div class="mb-6">
<div class="flex items-center justify-between mb-3">
<span class="text-sm text-dark-text-muted font-mono">Uploading...</span>
<span class="text-sm text-accent-primary font-mono font-medium">${Math.round(this.uploadProgress)}%</span>
<span class="text-sm text-muted font-mono">Uploading...</span>
<span class="text-sm text-primary font-mono font-medium">${Math.round(this.uploadProgress)}%</span>
</div>
<div class="w-full bg-dark-bg-secondary rounded-full h-2 overflow-hidden">
<div class="w-full bg-secondary rounded-full h-2 overflow-hidden">
<div
class="bg-gradient-to-r from-accent-primary to-accent-primary-light h-2 rounded-full transition-all duration-300 shadow-glow-primary-sm"
class="bg-gradient-to-r from-primary to-primary-light h-2 rounded-full transition-all duration-300 shadow-glow-sm"
style="width: ${this.uploadProgress}%"
></div>
</div>
@ -242,7 +242,7 @@ export class FilePicker extends LitElement {
<div class="space-y-4">
<button
@click=${this.handleFileClick}
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"
class="w-full bg-primary text-bg font-medium py-4 px-6 rounded-lg flex items-center justify-center gap-3 transition-all duration-200 hover:bg-primary-light hover:shadow-glow active:scale-95"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<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"/>
@ -253,10 +253,10 @@ export class FilePicker extends LitElement {
`
}
<div class="mt-6 pt-6 border-t border-dark-border">
<div class="mt-6 pt-6 border-t border-base">
<button
@click=${this.handleCancel}
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"
class="w-full bg-secondary border border-base text-primary font-mono py-3 px-6 rounded-lg transition-all duration-200 hover:bg-surface hover:border-primary active:scale-95"
?disabled=${this.uploading}
>
Cancel

View file

@ -16,7 +16,7 @@ export class FullHeader extends HeaderBase {
return html`
<div
class="app-header bg-dark-bg-secondary border-b border-dark-border p-3"
class="app-header bg-bg-secondary border-b border-border p-3"
style="padding-top: max(0.75rem, calc(0.75rem + env(safe-area-inset-top)));"
>
<div class="flex items-center justify-between">
@ -27,10 +27,10 @@ export class FullHeader extends HeaderBase {
>
<terminal-icon size="24"></terminal-icon>
<div class="flex items-baseline gap-2">
<h1 class="text-xl font-bold text-accent-green font-mono group-hover:underline">
<h1 class="text-xl font-bold text-primary font-mono group-hover:underline">
VibeTunnel
</h1>
<p class="text-dark-text-muted text-xs font-mono">
<p class="text-text-muted text-xs font-mono">
(${runningSessions.length})
</p>
</div>
@ -41,7 +41,7 @@ export class FullHeader extends HeaderBase {
@open-settings=${() => this.dispatchEvent(new CustomEvent('open-settings'))}
></notification-status>
<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"
class="p-2 text-text border border-border hover:border-primary hover:text-primary rounded-lg transition-all duration-200"
@click=${() => this.dispatchEvent(new CustomEvent('open-file-browser'))}
title="Browse Files (⌘O)"
>
@ -52,7 +52,7 @@ export class FullHeader extends HeaderBase {
</svg>
</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"
class="p-2 text-text border border-border hover:border-primary hover:text-primary rounded-lg transition-all duration-200"
@click=${this.handleScreenshare}
title="Start Screenshare"
>
@ -64,7 +64,7 @@ export class FullHeader extends HeaderBase {
</svg>
</button>
<button
class="p-2 bg-accent-green text-dark-bg hover:bg-accent-green-light rounded-lg transition-all duration-200 vt-create-button"
class="p-2 bg-primary text-text-bright hover:bg-primary-light rounded-lg transition-all duration-200 vt-create-button"
@click=${this.handleCreateSession}
title="Create New Session"
data-testid="create-session-button"
@ -89,7 +89,7 @@ export class FullHeader extends HeaderBase {
return html`
<div class="user-menu-container relative flex-shrink-0">
<button
class="font-mono text-sm px-3 py-2 text-dark-text border border-dark-border hover:bg-dark-bg-tertiary hover:text-dark-text rounded-lg transition-all duration-200 flex items-center gap-2"
class="font-mono text-sm px-3 py-2 text-text border border-border hover:bg-bg-tertiary hover:text-text rounded-lg transition-all duration-200 flex items-center gap-2"
@click=${this.toggleUserMenu}
title="User menu"
>
@ -117,13 +117,13 @@ export class FullHeader extends HeaderBase {
this.showUserMenu
? html`
<div
class="absolute right-0 top-full mt-1 bg-dark-surface border border-dark-border rounded-lg shadow-lg py-1 z-50 min-w-36"
class="absolute right-0 top-full mt-1 bg-surface border border-border rounded-lg shadow-lg py-1 z-50 min-w-36"
>
<div class="px-3 py-2 text-sm text-dark-text-muted border-b border-dark-border">
<div class="px-3 py-2 text-sm text-text-muted border-b border-border">
${this.authMethod || 'authenticated'}
</div>
<button
class="w-full text-left px-3 py-2 text-sm font-mono text-status-warning hover:bg-dark-bg-secondary hover:text-status-error"
class="w-full text-left px-3 py-2 text-sm font-mono text-status-warning hover:bg-bg-secondary hover:text-status-error"
@click=${this.handleLogout}
>
Logout

View file

@ -69,8 +69,8 @@ export class InlineEdit extends LitElement {
}
input {
background: var(--dark-bg-tertiary, #1a1a1a);
border: 1px solid var(--dark-border, #333);
background: rgb(var(--color-bg-tertiary));
border: 1px solid rgb(var(--color-border));
color: inherit;
font: inherit;
padding: 0.125rem 0.25rem;
@ -81,7 +81,7 @@ export class InlineEdit extends LitElement {
}
input:focus {
border-color: var(--accent-green, #10b981);
border-color: rgb(var(--color-primary));
}
.action-buttons {
@ -96,7 +96,7 @@ export class InlineEdit extends LitElement {
cursor: pointer;
padding: 0.125rem;
border-radius: 0.25rem;
color: var(--dark-text-muted, #999);
color: rgb(var(--color-text-muted));
transition: all 0.2s;
display: flex;
align-items: center;
@ -106,24 +106,24 @@ export class InlineEdit extends LitElement {
}
button:hover {
background: var(--dark-bg-tertiary, #1a1a1a);
background: rgb(var(--color-bg-tertiary));
}
button.save {
color: var(--accent-green, #10b981);
color: rgb(var(--color-primary));
}
button.save:hover {
background: var(--accent-green, #10b981);
background: rgb(var(--color-primary));
background-opacity: 0.2;
}
button.cancel {
color: var(--status-error, #ef4444);
color: rgb(var(--color-status-error));
}
button.cancel:hover {
background: var(--status-error, #ef4444);
background: rgb(var(--color-status-error));
background-opacity: 0.2;
}
`;

View file

@ -264,27 +264,27 @@ export class LogViewer extends LitElement {
/* Show scrollbar on hover */
.log-container:hover::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
background: rgb(var(--color-text-bright) / 0.2);
}
.log-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
background: rgb(var(--color-text-bright) / 0.3);
}
/* Firefox */
.log-container:hover {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
scrollbar-color: rgb(var(--color-text-bright) / 0.2) transparent;
}
</style>
`;
if (this.loading) {
return html`
<div class="flex items-center justify-center h-screen bg-dark-bg text-dark-text">
<div class="flex items-center justify-center h-screen bg-base text-primary">
<div class="text-center">
<div
class="animate-spin rounded-full h-12 w-12 border-4 border-accent-green border-t-transparent mb-4"
class="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent mb-4"
></div>
<div>Loading logs...</div>
</div>
@ -296,16 +296,16 @@ export class LogViewer extends LitElement {
return html`
${scrollbarStyles}
<div class="flex flex-col h-full bg-dark-bg text-dark-text font-mono">
<div class="flex flex-col h-full bg-base text-primary font-mono">
<!-- Header - single row on desktop, two rows on mobile -->
<div class="bg-dark-bg-secondary border-b border-dark-border p-3 sm:p-4">
<div class="bg-secondary border-b border-base p-3 sm:p-4">
<!-- Mobile layout (two rows) -->
<div class="sm:hidden">
<!-- Top row with back button and title -->
<div class="flex items-center gap-2 mb-3">
<!-- Back button -->
<button
class="p-2 bg-dark-bg border border-dark-border rounded text-sm text-dark-text hover:border-accent-green hover:text-accent-green transition-colors flex items-center gap-1 flex-shrink-0"
class="p-2 bg-base border border-base rounded text-sm text-primary hover:border-primary hover:text-primary transition-colors flex items-center gap-1 flex-shrink-0"
@click=${() => {
window.location.href = '/';
}}
@ -323,7 +323,7 @@ export class LogViewer extends LitElement {
</button>
<h1
class="text-base font-bold text-accent-green flex items-center gap-2 flex-shrink-0"
class="text-base font-bold text-primary flex items-center gap-2 flex-shrink-0"
>
<terminal-icon size="20"></terminal-icon>
<span>System Logs</span>
@ -334,8 +334,8 @@ export class LogViewer extends LitElement {
<button
class="p-2 text-xs uppercase font-bold rounded transition-colors ${
this.autoScroll
? 'bg-accent-green text-dark-bg'
: 'bg-dark-bg-tertiary text-dark-text-muted border border-dark-border'
? 'bg-primary text-base'
: 'bg-tertiary text-muted border border-base'
}"
@click=${() => {
this.autoScroll = !this.autoScroll;
@ -361,7 +361,7 @@ export class LogViewer extends LitElement {
<!-- Search input -->
<input
type="text"
class="px-3 py-1.5 bg-dark-bg border border-dark-border rounded text-sm text-dark-text placeholder-dark-text-muted focus:outline-none focus:border-accent-green transition-colors w-full"
class="px-3 py-1.5 bg-base border border-base rounded text-sm text-primary placeholder-muted focus:outline-none focus:border-primary transition-colors w-full"
placeholder="Filter logs..."
.value=${this.filter}
@input=${(e: Event) => {
@ -379,13 +379,13 @@ export class LogViewer extends LitElement {
class="px-1.5 py-1 text-xs uppercase font-bold rounded transition-colors ${
this.levelFilter.has(level)
? level === 'error'
? 'bg-status-error text-dark-bg'
? 'bg-status-error bg-opacity-20 text-status-error border border-status-error'
: level === 'warn'
? 'bg-status-warning text-dark-bg'
? 'bg-status-warning bg-opacity-20 text-status-warning border border-status-warning'
: level === 'debug'
? 'bg-dark-text-muted text-dark-bg'
: 'bg-dark-text text-dark-bg'
: 'bg-dark-bg-tertiary text-dark-text-muted border border-dark-border'
? 'bg-bg-tertiary text-text-muted border border-border'
: 'bg-primary bg-opacity-20 text-primary border border-primary'
: 'bg-bg-tertiary text-text-muted border border-border'
}"
@click=${() => this.toggleLevel(level)}
title="${level} logs"
@ -409,8 +409,8 @@ export class LogViewer extends LitElement {
<button
class="px-1.5 py-1 text-xs uppercase font-bold rounded transition-colors ${
this.showClient
? 'bg-orange-500 text-dark-bg'
: 'bg-dark-bg-tertiary text-dark-text-muted border border-dark-border'
? 'bg-status-warning bg-opacity-20 text-status-warning border border-status-warning'
: 'bg-bg-tertiary text-text-muted border border-border'
}"
@click=${() => {
this.showClient = !this.showClient;
@ -422,8 +422,8 @@ export class LogViewer extends LitElement {
<button
class="px-1.5 py-1 text-xs uppercase font-bold rounded transition-colors ${
this.showServer
? 'bg-accent-green text-dark-bg'
: 'bg-dark-bg-tertiary text-dark-text-muted border border-dark-border'
? 'bg-primary bg-opacity-20 text-primary border border-primary'
: 'bg-bg-tertiary text-text-muted border border-border'
}"
@click=${() => {
this.showServer = !this.showServer;
@ -441,7 +441,7 @@ export class LogViewer extends LitElement {
<div class="hidden sm:flex items-center gap-3">
<!-- Back button -->
<button
class="px-3 py-1.5 bg-dark-bg border border-dark-border rounded text-sm text-dark-text hover:border-accent-green hover:text-accent-green transition-colors flex items-center gap-2 flex-shrink-0"
class="px-3 py-1.5 bg-bg border border-border rounded text-sm text-primary hover:border-primary hover:text-primary transition-colors flex items-center gap-2 flex-shrink-0"
@click=${() => {
window.location.href = '/';
}}
@ -459,7 +459,7 @@ export class LogViewer extends LitElement {
Back
</button>
<h1 class="text-lg font-bold text-accent-green flex items-center gap-2 flex-shrink-0">
<h1 class="text-lg font-bold text-primary flex items-center gap-2 flex-shrink-0">
<terminal-icon size="24"></terminal-icon>
<span>System Logs</span>
</h1>
@ -468,7 +468,7 @@ export class LogViewer extends LitElement {
<!-- Search input -->
<input
type="text"
class="px-3 py-1.5 bg-dark-bg border border-dark-border rounded text-sm text-dark-text placeholder-dark-text-muted focus:outline-none focus:border-accent-green transition-colors flex-1 sm:flex-initial sm:w-64 md:w-80"
class="px-3 py-1.5 bg-bg border border-border rounded text-sm text-primary placeholder-text-muted focus:outline-none focus:border-primary transition-colors flex-1 sm:flex-initial sm:w-64 md:w-80"
placeholder="Filter logs..."
.value=${this.filter}
@input=${(e: Event) => {
@ -484,13 +484,13 @@ export class LogViewer extends LitElement {
class="px-2 py-1 text-xs uppercase font-bold rounded transition-colors ${
this.levelFilter.has(level)
? level === 'error'
? 'bg-status-error text-dark-bg'
? 'bg-status-error bg-opacity-20 text-status-error border border-status-error'
: level === 'warn'
? 'bg-status-warning text-dark-bg'
? 'bg-status-warning bg-opacity-20 text-status-warning border border-status-warning'
: level === 'debug'
? 'bg-dark-text-muted text-dark-bg'
: 'bg-dark-text text-dark-bg'
: 'bg-dark-bg-tertiary text-dark-text-muted border border-dark-border'
? 'bg-bg-tertiary text-text-muted border border-border'
: 'bg-primary bg-opacity-20 text-primary border border-primary'
: 'bg-bg-tertiary text-text-muted border border-border'
}"
@click=${() => this.toggleLevel(level)}
>
@ -505,8 +505,8 @@ export class LogViewer extends LitElement {
<button
class="px-2 py-1 text-xs uppercase font-bold rounded transition-colors ${
this.showClient
? 'bg-orange-500 text-dark-bg'
: 'bg-dark-bg-tertiary text-dark-text-muted border border-dark-border'
? 'bg-status-warning bg-opacity-20 text-status-warning border border-status-warning'
: 'bg-bg-tertiary text-text-muted border border-border'
}"
@click=${() => {
this.showClient = !this.showClient;
@ -517,8 +517,8 @@ export class LogViewer extends LitElement {
<button
class="px-2 py-1 text-xs uppercase font-bold rounded transition-colors ${
this.showServer
? 'bg-accent-green text-dark-bg'
: 'bg-dark-bg-tertiary text-dark-text-muted border border-dark-border'
? 'bg-primary bg-opacity-20 text-primary border border-primary'
: 'bg-bg-tertiary text-text-muted border border-border'
}"
@click=${() => {
this.showServer = !this.showServer;
@ -532,8 +532,8 @@ export class LogViewer extends LitElement {
<button
class="px-3 py-1 text-xs uppercase font-bold rounded transition-colors ${
this.autoScroll
? 'bg-accent-green text-dark-bg'
: 'bg-dark-bg-tertiary text-dark-text-muted border border-dark-border'
? 'bg-primary bg-opacity-20 text-primary border border-primary'
: 'bg-bg-tertiary text-text-muted border border-border'
}"
@click=${() => {
this.autoScroll = !this.autoScroll;
@ -547,12 +547,12 @@ export class LogViewer extends LitElement {
<!-- Log container -->
<div
class="log-container flex-1 overflow-y-auto p-4 bg-dark-bg font-mono text-xs leading-relaxed"
class="log-container flex-1 overflow-y-auto p-4 bg-bg font-mono text-xs leading-relaxed"
>
${
this.filteredLogs.length === 0
? html`
<div class="flex items-center justify-center h-full text-dark-text-muted">
<div class="flex items-center justify-center h-full text-text-muted">
<div class="text-center">
<div>No logs to display</div>
</div>
@ -564,14 +564,14 @@ export class LogViewer extends LitElement {
return html`
<div
class="group hover:bg-dark-bg-secondary/50 transition-colors rounded ${
log.isClient ? 'bg-orange-500/5 pl-2' : 'pl-2'
class="group hover:bg-bg-secondary/50 transition-colors rounded ${
log.isClient ? 'bg-status-warning/5 pl-2' : 'pl-2'
}"
>
<!-- Desktop layout (hidden on mobile) -->
<div class="hidden sm:flex items-start gap-2 py-0.5">
<!-- Timestamp -->
<span class="text-dark-text-muted w-16 flex-shrink-0 opacity-50"
<span class="text-text-muted w-16 flex-shrink-0 opacity-50"
>${this.formatRelativeTime(log.timestamp)}</span
>
@ -579,12 +579,12 @@ export class LogViewer extends LitElement {
<span
class="w-10 text-center font-mono uppercase tracking-wider flex-shrink-0 ${
log.level === 'error'
? 'text-red-500 bg-red-500/20 px-1 rounded font-bold'
? 'text-status-error bg-status-error/20 px-1 rounded font-bold'
: log.level === 'warn'
? 'text-yellow-500 bg-yellow-500/20 px-1 rounded font-bold'
? 'text-status-warning bg-status-warning/20 px-1 rounded font-bold'
: log.level === 'debug'
? 'text-gray-600'
: 'text-gray-500'
? 'text-text-muted'
: 'text-primary'
}"
>${
log.level === 'error'
@ -600,29 +600,29 @@ export class LogViewer extends LitElement {
<!-- Source indicator -->
<span
class="flex-shrink-0 ${
log.isClient ? 'text-orange-400 font-bold' : 'text-green-600'
log.isClient ? 'text-status-warning font-bold' : 'text-primary'
}"
>${log.isClient ? '◆ C' : '▸ S'}</span
>
<!-- Module -->
<span class="text-gray-600 flex-shrink-0 font-mono">${log.module}</span>
<span class="text-text-muted flex-shrink-0 font-mono">${log.module}</span>
<!-- Separator -->
<span class="text-gray-700 flex-shrink-0"></span>
<span class="text-text-muted flex-shrink-0"></span>
<!-- Message -->
<span
class="flex-1 ${
log.level === 'error'
? 'text-red-400'
? 'text-status-error'
: log.level === 'warn'
? 'text-yellow-400'
? 'text-status-warning'
: log.level === 'debug'
? 'text-gray-600'
? 'text-text-muted'
: log.isClient
? 'text-orange-200'
: 'text-gray-300'
? 'text-status-warning opacity-80'
: 'text-primary'
}"
>${messageLines[0]}</span
>
@ -631,37 +631,37 @@ export class LogViewer extends LitElement {
<!-- Mobile layout (visible only on mobile) -->
<div class="sm:hidden py-1">
<div class="flex items-center gap-2 text-xs">
<span class="text-dark-text-muted opacity-50"
<span class="text-text-muted opacity-50"
>${this.formatRelativeTime(log.timestamp)}</span
>
<span
class="${
log.level === 'error'
? 'text-red-500 font-bold'
? 'text-status-error font-bold'
: log.level === 'warn'
? 'text-yellow-500 font-bold'
? 'text-status-warning font-bold'
: log.level === 'debug'
? 'text-gray-600'
: 'text-gray-500'
? 'text-text-muted'
: 'text-primary'
} uppercase"
>${log.level}</span
>
<span class="${log.isClient ? 'text-orange-400' : 'text-green-600'}"
<span class="${log.isClient ? 'text-status-warning' : 'text-primary'}"
>${log.isClient ? '[C]' : '[S]'}</span
>
<span class="text-gray-600">${log.module}</span>
<span class="text-text-muted">${log.module}</span>
</div>
<div
class="mt-1 ${
log.level === 'error'
? 'text-red-400'
? 'text-status-error'
: log.level === 'warn'
? 'text-yellow-400'
? 'text-status-warning'
: log.level === 'debug'
? 'text-gray-600'
? 'text-text-muted'
: log.isClient
? 'text-orange-200'
: 'text-gray-300'
? 'text-status-warning opacity-80'
: 'text-primary'
}"
>
${messageLines[0]}
@ -673,10 +673,10 @@ export class LogViewer extends LitElement {
<div
class="hidden sm:block ml-36 ${
log.level === 'error'
? 'text-red-400'
? 'text-status-error'
: log.level === 'warn'
? 'text-yellow-400'
: 'text-gray-500'
? 'text-status-warning'
: 'text-text-muted'
}"
>
${messageLines
@ -686,10 +686,10 @@ export class LogViewer extends LitElement {
<div
class="sm:hidden mt-1 ${
log.level === 'error'
? 'text-red-400'
? 'text-status-error'
: log.level === 'warn'
? 'text-yellow-400'
: 'text-gray-500'
? 'text-status-warning'
: 'text-text-muted'
}"
>
${messageLines
@ -707,23 +707,21 @@ export class LogViewer extends LitElement {
<!-- Footer -->
<div
class="flex items-center justify-between p-3 bg-dark-bg-secondary border-t border-dark-border text-xs"
class="flex items-center justify-between p-3 bg-bg-secondary border-t border-border text-xs"
>
<div class="text-dark-text-muted">
<div class="text-text-muted">
${this.filteredLogs.length} / ${this.logs.length} logs
${
this.logSize ? html` <span class="text-dark-text-muted">• ${this.logSize}</span>` : ''
}
${this.logSize ? html` <span class="text-text-muted">• ${this.logSize}</span>` : ''}
</div>
<div class="flex gap-2">
<button
class="px-3 py-1 bg-dark-bg border border-dark-border rounded hover:border-accent-green hover:text-accent-green transition-colors"
class="px-3 py-1 bg-bg border border-border rounded hover:border-primary hover:text-primary transition-colors"
@click=${this.downloadLogs}
>
Download
</button>
<button
class="px-3 py-1 bg-dark-bg border border-status-error text-status-error rounded hover:bg-status-error hover:text-dark-bg transition-colors"
class="px-3 py-1 bg-bg border border-status-error text-status-error rounded hover:bg-status-error hover:text-text-bright transition-colors"
@click=${this.clearLogs}
>
Clear

View file

@ -74,7 +74,7 @@ export class MagicWandButton extends LitElement {
return html`
<button
class="btn-ghost text-accent-primary ${buttonClasses} rounded-md transition-all hover:bg-dark-bg-elevated hover:shadow-sm hover:scale-110 disabled:opacity-50"
class="btn-ghost text-primary ${buttonClasses} rounded-md transition-all hover:bg-elevated hover:shadow-sm hover:scale-110 disabled:opacity-50"
@click=${this.handleClick}
?disabled=${this.sending}
title="Send prompt to update terminal title"

View file

@ -296,7 +296,7 @@ export class MonacoEditor extends LitElement {
private setupTheme() {
// Use the default VS dark theme
if (!window.monaco) return;
window.monaco.editor.setTheme('vs-dark');
// Theme is already handled by monaco-loader.ts with theme change observer
}
private detectLanguage(): string {
@ -472,14 +472,14 @@ export class MonacoEditor extends LitElement {
<div
class="editor-container"
${ref(this.containerRef)}
style="width: 100%; height: 100%; position: relative; background: #1e1e1e;"
style="width: 100%; height: 100%; position: relative; background: rgb(var(--color-bg-secondary));"
>
${
this.isLoading
? html`
<div
class="loading"
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #666; font-family: ui-monospace, monospace;"
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: rgb(var(--color-text-muted)); font-family: ui-monospace, monospace;"
>
Loading editor...
</div>
@ -491,18 +491,18 @@ export class MonacoEditor extends LitElement {
? html`
<button
class="mode-toggle"
style="position: absolute; top: 10px; right: 10px; z-index: 10; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer;"
style="position: absolute; top: 10px; right: 10px; z-index: 10; background: rgb(var(--color-surface)); border: 1px solid rgb(var(--color-border)); color: rgb(var(--color-text)); padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer;"
@click=${this.toggleDiffMode}
title="Toggle between inline and side-by-side diff"
@mouseenter=${(e: MouseEvent) => {
const btn = e.target as HTMLButtonElement;
btn.style.background = 'rgba(255, 255, 255, 0.2)';
btn.style.borderColor = 'rgba(255, 255, 255, 0.3)';
btn.style.background = 'rgb(var(--color-surface-hover))';
btn.style.borderColor = 'rgb(var(--color-border-focus))';
}}
@mouseleave=${(e: MouseEvent) => {
const btn = e.target as HTMLButtonElement;
btn.style.background = 'rgba(255, 255, 255, 0.1)';
btn.style.borderColor = 'rgba(255, 255, 255, 0.2)';
btn.style.background = 'rgb(var(--color-surface))';
btn.style.borderColor = 'rgb(var(--color-border))';
}}
>
${this.diffMode === 'inline' ? 'Side by Side' : 'Inline'}

View file

@ -110,7 +110,7 @@ export class NotificationStatus extends LitElement {
return html`
<button
@click=${this.handleClick}
class="p-2 ${color} hover:text-dark-text transition-colors relative"
class="p-2 ${color} hover:text-primary transition-colors relative"
title="${tooltip}"
>
${this.renderIcon()}

View file

@ -11,17 +11,17 @@ export class ScreencapSidebar extends LitElement {
:host {
display: block;
height: 100%;
background: #0f0f0f;
border-right: 1px solid #2a2a2a;
background: rgb(var(--color-bg));
border-right: 1px solid rgb(var(--color-border));
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #2a2a2a #0f0f0f;
scrollbar-color: rgb(var(--color-border)) rgb(var(--color-bg));
}
.sidebar-header {
padding: 1rem;
border-bottom: 1px solid #2a2a2a;
background: linear-gradient(to bottom, #141414, #0f0f0f);
border-bottom: 1px solid rgb(var(--color-border));
background: linear-gradient(to bottom, rgb(var(--color-bg-secondary)), rgb(var(--color-bg-base)));
display: flex;
align-items: center;
justify-content: space-between;
@ -32,7 +32,7 @@ export class ScreencapSidebar extends LitElement {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: #e4e4e4;
color: rgb(var(--color-text));
display: flex;
align-items: center;
gap: 0.5rem;
@ -48,17 +48,17 @@ export class ScreencapSidebar extends LitElement {
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
color: #a3a3a3;
color: rgb(var(--color-text-muted));
font-size: 0.875rem;
font-weight: 500;
}
.refresh-btn {
padding: 0.25rem 0.5rem;
border: 1px solid #2a2a2a;
border: 1px solid rgb(var(--color-border));
border-radius: 0.375rem;
background: transparent;
color: #a3a3a3;
color: rgb(var(--color-text-muted));
cursor: pointer;
transition: all 0.2s;
font-size: 0.75rem;
@ -68,8 +68,8 @@ export class ScreencapSidebar extends LitElement {
}
.refresh-btn:hover {
border-color: #10B981;
color: #10B981;
border-color: rgb(var(--color-primary));
color: rgb(var(--color-primary));
}
.refresh-btn.loading {
@ -85,15 +85,15 @@ export class ScreencapSidebar extends LitElement {
}
.process-item {
background: #1a1a1a;
border: 1px solid #2a2a2a;
background: rgb(var(--color-surface));
border: 1px solid rgb(var(--color-border));
border-radius: 0.5rem;
overflow: hidden;
transition: all 0.2s;
}
.process-item:hover {
border-color: #3a3a3a;
border-color: rgb(var(--color-border-light));
}
.process-header {
@ -106,14 +106,14 @@ export class ScreencapSidebar extends LitElement {
}
.process-header:hover {
background: #262626;
background: rgb(var(--color-bg-elevated));
}
.process-icon {
width: 24px;
height: 24px;
border-radius: 0.375rem;
background: #262626;
background: rgb(var(--color-bg-elevated));
display: flex;
align-items: center;
justify-content: center;
@ -132,7 +132,7 @@ export class ScreencapSidebar extends LitElement {
.process-name {
font-weight: 500;
color: #e4e4e4;
color: rgb(var(--color-text));
font-size: 0.875rem;
overflow: hidden;
text-overflow: ellipsis;
@ -144,7 +144,7 @@ export class ScreencapSidebar extends LitElement {
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: #737373;
color: rgb(var(--color-text-dim));
margin-top: 0.125rem;
}
@ -152,8 +152,8 @@ export class ScreencapSidebar extends LitElement {
display: inline-flex;
align-items: center;
justify-content: center;
background: #262626;
color: #a3a3a3;
background: rgb(var(--color-bg-elevated));
color: rgb(var(--color-text-muted));
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
@ -164,7 +164,7 @@ export class ScreencapSidebar extends LitElement {
.expand-icon {
width: 16px;
height: 16px;
color: #737373;
color: rgb(var(--color-text-dim));
transition: transform 0.2s;
}
@ -182,7 +182,7 @@ export class ScreencapSidebar extends LitElement {
flex-direction: column;
gap: 0.25rem;
padding: 0.5rem 0.75rem 0.75rem 0.75rem;
background: #0a0a0a;
background: rgb(var(--color-bg));
}
.process-item.expanded .window-list {
@ -194,26 +194,26 @@ export class ScreencapSidebar extends LitElement {
flex-direction: column;
align-items: stretch;
padding: 0.75rem;
background: #141414;
border: 1px solid #262626;
background: rgb(var(--color-bg-secondary));
border: 1px solid rgb(var(--color-border));
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
color: #e4e4e4;
color: rgb(var(--color-text));
gap: 0.25rem;
min-height: 3.5rem;
}
.window-item:hover {
background: #1a1a1a;
border-color: #3a3a3a;
background: rgb(var(--color-surface));
border-color: rgb(var(--color-border-light));
}
.window-item.selected {
background: #10B981;
border-color: #10B981;
color: #0a0a0a;
background: rgb(var(--color-primary));
border-color: rgb(var(--color-primary));
color: rgb(var(--color-bg));
font-weight: 500;
}
@ -238,30 +238,30 @@ export class ScreencapSidebar extends LitElement {
align-items: center;
justify-content: space-between;
padding: 0.75rem;
background: #1a1a1a;
border: 1px solid #2a2a2a;
background: rgb(var(--color-surface));
border: 1px solid rgb(var(--color-border));
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.display-item:hover {
background: #262626;
border-color: #3a3a3a;
background: rgb(var(--color-bg-elevated));
border-color: rgb(var(--color-border-light));
}
.display-item.selected {
background: #10B981;
border-color: #10B981;
background: rgb(var(--color-primary));
border-color: rgb(var(--color-primary));
}
.display-item.selected .display-name {
color: #0a0a0a;
color: rgb(var(--color-bg));
font-weight: 500;
}
.display-item.selected .display-size {
color: #0a0a0a;
color: rgb(var(--color-bg));
opacity: 0.8;
}
@ -274,7 +274,7 @@ export class ScreencapSidebar extends LitElement {
.display-icon {
width: 32px;
height: 24px;
background: #262626;
background: rgb(var(--color-bg-elevated));
border-radius: 0.25rem;
display: flex;
align-items: center;
@ -283,28 +283,28 @@ export class ScreencapSidebar extends LitElement {
}
.display-item.selected .display-icon {
background: rgba(255, 255, 255, 0.2);
background: rgb(var(--color-text-bright) / 0.2);
}
.display-name {
font-weight: 500;
color: #e4e4e4;
color: rgb(var(--color-text));
font-size: 0.875rem;
}
.display-size {
font-size: 0.75rem;
color: #737373;
color: rgb(var(--color-text-dim));
margin-top: 0.125rem;
}
.all-displays-btn {
width: 100%;
padding: 0.75rem;
border: 1px solid #2a2a2a;
border: 1px solid rgb(var(--color-border));
border-radius: 0.5rem;
background: linear-gradient(135deg, #1a1a1a, #262626);
color: #e4e4e4;
background: linear-gradient(135deg, rgb(var(--color-bg-secondary)), rgb(var(--color-bg-tertiary)));
color: rgb(var(--color-text));
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
@ -317,14 +317,14 @@ export class ScreencapSidebar extends LitElement {
}
.all-displays-btn:hover {
border-color: #10B981;
background: linear-gradient(135deg, #262626, #2a2a2a);
border-color: rgb(var(--color-primary));
background: linear-gradient(135deg, rgb(var(--color-bg-tertiary)), rgb(var(--color-surface)));
}
.all-displays-btn.selected {
background: #10B981;
border-color: #10B981;
color: #0a0a0a;
background: rgb(var(--color-primary));
border-color: rgb(var(--color-primary));
color: rgb(var(--color-bg));
font-weight: 500;
}
`;
@ -474,7 +474,7 @@ export class ScreencapSidebar extends LitElement {
${
process.iconData
? html`<img src="data:image/png;base64,${process.iconData}" alt="${process.processName}">`
: html`<svg width="20" height="20" viewBox="0 0 24 24" fill="#737373">
: html`<svg width="20" height="20" viewBox="0 0 24 24" fill="rgb(var(--color-text-dim))">
<rect x="3" y="3" width="18" height="18" rx="2" />
</svg>`
}

View file

@ -25,13 +25,13 @@ export class ScreencapStats extends LitElement {
position: absolute;
top: 1rem;
right: 1rem;
background: rgba(15, 15, 15, 0.95);
background: rgba(var(--color-bg), 0.95);
backdrop-filter: blur(10px);
border: 1px solid #2a2a2a;
border: 1px solid rgb(var(--color-border));
border-radius: 0.75rem;
padding: 1rem;
min-width: 250px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
box-shadow: 0 4px 6px rgb(var(--color-bg-base) / 0.3);
font-size: 0.875rem;
}
@ -39,7 +39,7 @@ export class ScreencapStats extends LitElement {
margin: 0 0 0.75rem 0;
font-size: 1rem;
font-weight: 600;
color: #e4e4e4;
color: rgb(var(--color-text));
display: flex;
align-items: center;
gap: 0.5rem;
@ -50,7 +50,7 @@ export class ScreencapStats extends LitElement {
justify-content: space-between;
align-items: center;
padding: 0.375rem 0;
border-bottom: 1px solid rgba(42, 42, 42, 0.5);
border-bottom: 1px solid rgba(var(--color-border), 0.5);
}
.stat-row:last-child {
@ -58,43 +58,43 @@ export class ScreencapStats extends LitElement {
}
.stat-label {
color: #a3a3a3;
color: rgb(var(--color-text-muted));
font-weight: 500;
}
.stat-value {
color: #e4e4e4;
color: rgb(var(--color-text));
font-variant-numeric: tabular-nums;
}
.stat-value.codec-h264,
.stat-value.codec-h265 {
color: #10B981;
color: rgb(var(--color-primary));
}
.stat-value.codec-vp8,
.stat-value.codec-vp9 {
color: #3B82F6;
color: rgb(var(--color-status-info));
}
.stat-value.codec-av1 {
color: #8B5CF6;
color: rgb(var(--color-status-info));
}
.stat-value.latency-good {
color: #10B981;
color: rgb(var(--color-primary));
}
.stat-value.latency-warning {
color: #F59E0B;
color: rgb(var(--color-status-warning));
}
.stat-value.latency-bad {
color: #EF4444;
color: rgb(var(--color-status-error));
}
.loading-message {
color: #a3a3a3;
color: rgb(var(--color-text-muted));
text-align: center;
padding: 1rem;
}
@ -121,19 +121,19 @@ export class ScreencapStats extends LitElement {
}
.quality-excellent .quality-dot {
background: #10B981;
background: rgb(var(--color-primary));
}
.quality-good .quality-dot {
background: #3B82F6;
background: rgb(var(--color-status-info));
}
.quality-fair .quality-dot {
background: #F59E0B;
background: rgb(var(--color-status-warning));
}
.quality-poor .quality-dot {
background: #EF4444;
background: rgb(var(--color-status-error));
}
`;

View file

@ -33,8 +33,8 @@ export class ScreencapView extends LitElement {
display: flex;
flex-direction: column;
height: 100vh;
background: #0a0a0a;
color: #e4e4e4;
background: rgb(var(--color-bg));
color: rgb(var(--color-text));
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
/* Honor safe areas on mobile devices */
@ -48,17 +48,17 @@ export class ScreencapView extends LitElement {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
background: linear-gradient(to right, #141414, #1f1f1f);
border-bottom: 1px solid #2a2a2a;
background: linear-gradient(to right, rgb(var(--color-bg-secondary)), rgb(var(--color-bg-tertiary)));
border-bottom: 1px solid rgb(var(--color-border));
gap: 1rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
box-shadow: 0 1px 3px rgb(var(--color-bg-base) / 0.3);
}
.header h1 {
margin: 0;
font-size: 1.25rem;
font-weight: 700;
color: #10B981;
color: rgb(var(--color-primary));
display: flex;
align-items: center;
gap: 0.5rem;
@ -72,10 +72,10 @@ export class ScreencapView extends LitElement {
.btn {
padding: 0.5rem 1rem;
border: 1px solid #2a2a2a;
border: 1px solid rgb(var(--color-border));
border-radius: 0.5rem;
background: transparent;
color: #e4e4e4;
color: rgb(var(--color-text));
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
@ -87,31 +87,31 @@ export class ScreencapView extends LitElement {
}
.btn:hover {
border-color: #10B981;
color: #10B981;
border-color: rgb(var(--color-primary));
color: rgb(var(--color-primary));
}
.btn.primary {
background: #10B981;
color: #0a0a0a;
border-color: #10B981;
background: rgb(var(--color-primary));
color: rgb(var(--color-bg));
border-color: rgb(var(--color-primary));
font-weight: 500;
}
.btn.primary:hover {
background: #0D9668;
border-color: #0D9668;
background: rgb(var(--color-primary-hover));
border-color: rgb(var(--color-primary-hover));
}
.btn.danger {
background: #EF4444;
color: white;
border-color: #EF4444;
background: rgb(var(--color-status-error));
color: rgb(var(--color-text-bright));
border-color: rgb(var(--color-status-error));
}
.btn.danger:hover {
background: #DC2626;
border-color: #DC2626;
background: rgb(var(--color-status-error));
border-color: rgb(var(--color-status-error));
}
.main-container {
@ -147,7 +147,7 @@ export class ScreencapView extends LitElement {
display: flex;
align-items: center;
justify-content: center;
background: #0a0a0a;
background: rgb(var(--color-bg));
overflow: hidden;
padding: 1rem;
}
@ -171,11 +171,11 @@ export class ScreencapView extends LitElement {
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
background: rgb(var(--color-bg));
}
:host(:focus) {
outline: 2px solid #60a5fa;
outline: 2px solid rgb(var(--color-status-info));
outline-offset: -2px;
}
@ -198,7 +198,7 @@ export class ScreencapView extends LitElement {
}
video.capture-preview {
background: #000;
background: rgb(var(--color-bg));
}
.capture-overlay {
@ -213,23 +213,23 @@ export class ScreencapView extends LitElement {
.status-message {
font-size: 1.125rem;
color: #a3a3a3;
color: rgb(var(--color-text-muted));
max-width: 500px;
}
.status-message.error {
color: #EF4444;
color: rgb(var(--color-status-error));
font-weight: 500;
background: rgba(239, 68, 68, 0.1);
background: rgb(var(--color-status-error) / 0.1);
padding: 1rem 1.5rem;
border-radius: 0.5rem;
border: 1px solid rgba(239, 68, 68, 0.3);
border: 1px solid rgb(var(--color-status-error) / 0.3);
line-height: 1.6;
}
.status-message.loading,
.status-message.starting {
color: #F59E0B;
color: rgb(var(--color-status-warning));
}
.fps-indicator {
@ -244,14 +244,14 @@ export class ScreencapView extends LitElement {
display: flex;
align-items: center;
gap: 0.5rem;
color: #10B981;
color: rgb(var(--color-primary));
border: 1px solid rgba(16, 185, 129, 0.3);
}
.back-btn {
background: none;
border: none;
color: #a3a3a3;
color: rgb(var(--color-text-muted));
cursor: pointer;
padding: 0.5rem;
display: flex;
@ -262,13 +262,13 @@ export class ScreencapView extends LitElement {
}
.back-btn:hover {
color: #10B981;
color: rgb(var(--color-primary));
}
.toggle-btn {
background: none;
border: none;
color: #a3a3a3;
color: rgb(var(--color-text-muted));
cursor: pointer;
padding: 0.5rem;
display: flex;
@ -279,11 +279,11 @@ export class ScreencapView extends LitElement {
}
.toggle-btn:hover {
color: #e4e4e4;
color: rgb(var(--color-text));
}
.toggle-btn.active {
color: #10B981;
color: rgb(var(--color-primary));
}
.status-log {
@ -294,15 +294,15 @@ export class ScreencapView extends LitElement {
max-width: 600px;
max-height: 200px;
overflow-y: auto;
background: rgba(10, 10, 10, 0.95);
background: rgb(var(--color-bg-elevated) / 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(64, 64, 64, 0.3);
border: 1px solid rgb(var(--color-border) / 0.3);
border-radius: 0.5rem;
padding: 1rem;
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1.5;
color: #a3a3a3;
color: rgb(var(--color-text-muted));
}
.status-log-entry {
@ -312,7 +312,7 @@ export class ScreencapView extends LitElement {
}
.status-log-time {
color: #737373;
color: rgb(var(--color-text-dim));
flex-shrink: 0;
}
@ -320,10 +320,10 @@ export class ScreencapView extends LitElement {
flex: 1;
}
.status-log-entry.info { color: #60a5fa; }
.status-log-entry.success { color: #10b981; }
.status-log-entry.warning { color: #f59e0b; }
.status-log-entry.error { color: #ef4444; }
.status-log-entry.info { color: rgb(var(--color-status-info)); }
.status-log-entry.success { color: rgb(var(--color-status-success)); }
.status-log-entry.warning { color: rgb(var(--color-status-warning)); }
.status-log-entry.error { color: rgb(var(--color-status-error)); }
.switch {
display: flex;
@ -337,7 +337,7 @@ export class ScreencapView extends LitElement {
width: 36px;
height: 20px;
border-radius: 10px;
background: #3f3f46;
background: rgb(var(--color-surface-hover));
position: relative;
cursor: pointer;
transition: background-color 0.2s;
@ -349,14 +349,14 @@ export class ScreencapView extends LitElement {
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
background: rgb(var(--color-text-bright));
top: 2px;
left: 2px;
transition: transform 0.2s;
}
.switch input:checked {
background-color: #10B981;
background-color: rgb(var(--color-primary));
}
.switch input:checked::before {
@ -368,8 +368,8 @@ export class ScreencapView extends LitElement {
bottom: calc(1rem + env(safe-area-inset-bottom));
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: #a1a1aa;
background: rgb(var(--color-bg-base) / 0.8);
color: rgb(var(--color-text-muted));
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
@ -387,26 +387,26 @@ export class ScreencapView extends LitElement {
right: calc(20px + env(safe-area-inset-right));
width: 60px;
height: 60px;
background: #10B981;
background: rgb(var(--color-primary));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
box-shadow: 0 4px 6px rgb(var(--color-bg-base) / 0.3);
z-index: 1000;
cursor: pointer;
transition: all 0.2s;
}
.keyboard-button:hover {
background: #0D9668;
background: rgb(var(--color-primary-hover));
transform: scale(1.1);
}
.keyboard-button svg {
width: 28px;
height: 28px;
color: white;
color: rgb(var(--color-text-bright));
}
.mobile-keyboard-input {

View file

@ -371,9 +371,9 @@ export class SessionCard extends LitElement {
>
<!-- Compact Header -->
<div
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"
class="flex justify-between items-center px-3 py-2 border-b border-base bg-gradient-to-r from-secondary to-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-primary">
<inline-edit
.value=${this.session.name || this.session.command?.join(' ') || ''}
.placeholder=${this.session.command?.join(' ') || ''}
@ -392,7 +392,7 @@ export class SessionCard extends LitElement {
this.session.status === 'running' && isAIAssistantSession(this.session)
? html`
<button
class="bg-transparent border-0 p-0 cursor-pointer opacity-50 hover:opacity-100 transition-opacity duration-200 text-accent-primary"
class="bg-transparent border-0 p-0 cursor-pointer opacity-50 hover:opacity-100 transition-opacity duration-200 text-primary"
@click=${(e: Event) => {
e.stopPropagation();
this.handleMagicButton();
@ -456,10 +456,10 @@ export class SessionCard extends LitElement {
<!-- Terminal display (main content) -->
<div
class="session-preview bg-black overflow-hidden flex-1 relative ${
class="session-preview bg-bg overflow-hidden flex-1 relative ${
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);"
style="background: linear-gradient(to bottom, rgb(var(--color-bg)), rgb(var(--color-bg-secondary))); box-shadow: inset 0 1px 3px rgb(var(--color-bg) / 0.5);"
>
${
this.killing
@ -484,7 +484,7 @@ export class SessionCard extends LitElement {
<!-- Compact Footer -->
<div
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"
class="px-3 py-2 text-muted text-xs border-t border-base bg-gradient-to-r from-tertiary to-secondary"
>
<div class="flex justify-between items-center min-w-0">
<span
@ -498,7 +498,7 @@ export class SessionCard extends LitElement {
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-primary animate-pulse ml-1">●</span>`
: ''
}
</span>
@ -506,7 +506,7 @@ export class SessionCard extends LitElement {
this.session.pid
? html`
<span
class="cursor-pointer hover:text-accent-green transition-colors text-xs flex-shrink-0 ml-2 inline-flex items-center gap-1"
class="cursor-pointer hover:text-primary transition-colors text-xs flex-shrink-0 ml-2 inline-flex items-center gap-1"
@click=${this.handlePidClick}
title="Click to copy PID"
>
@ -549,7 +549,7 @@ export class SessionCard extends LitElement {
return 'text-status-error';
}
if (this.session.active === false) {
return 'text-dark-text-muted';
return 'text-muted';
}
return this.session.status === 'running' ? 'text-status-success' : 'text-status-warning';
}
@ -559,7 +559,7 @@ export class SessionCard extends LitElement {
return 'text-status-error';
}
if (this.session.active === false) {
return 'text-dark-text-muted';
return 'text-muted';
}
if (this.session.status === 'running' && this.session.activityStatus?.specificStatus) {
return 'text-status-warning';
@ -572,7 +572,7 @@ export class SessionCard extends LitElement {
return 'bg-status-error animate-pulse';
}
if (this.session.active === false) {
return 'bg-dark-text-muted';
return 'bg-muted';
}
if (this.session.status === 'running') {
if (this.session.activityStatus?.specificStatus) {

View file

@ -165,9 +165,6 @@ export class SessionCreateForm extends LitElement {
// Handle visibility changes
if (changedProperties.has('visible')) {
if (this.visible) {
// Remove any lingering modal-closing class that might make the modal invisible
document.body.classList.remove('modal-closing');
// Reset to defaults first to ensure clean state
this.workingDir = '~/';
this.command = 'zsh';
@ -409,28 +406,18 @@ export class SessionCreateForm extends LitElement {
return html``;
}
// Ensure modal-closing class is removed when rendering visible modal
if (this.visible) {
// Remove immediately
document.body.classList.remove('modal-closing');
// Also check if element has data-testid
requestAnimationFrame(() => {
document.body.classList.remove('modal-closing');
});
}
return html`
<div class="modal-backdrop flex items-center justify-center" @click=${this.handleBackdropClick} role="dialog" aria-modal="true">
<div
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; pointer-events: auto;"
style="pointer-events: auto;"
@click=${(e: Event) => e.stopPropagation()}
data-testid="session-create-modal"
>
<div class="p-3 sm:p-4 lg:p-6 mb-1 sm:mb-2 lg:mb-3 border-b border-dark-border relative bg-gradient-to-r from-dark-bg-secondary to-dark-bg-tertiary flex-shrink-0">
<div class="p-3 sm:p-4 lg:p-6 mb-1 sm:mb-2 lg:mb-3 border-b border-base relative bg-gradient-to-r from-secondary to-tertiary flex-shrink-0">
<h2 id="modal-title" class="text-primary text-base sm:text-lg lg:text-xl font-bold">New Session</h2>
<button
class="absolute top-2 right-2 sm:top-3 sm:right-3 lg:top-5 lg:right-5 text-dark-text-muted hover:text-dark-text transition-all duration-200 p-1.5 sm:p-2 hover:bg-dark-bg-tertiary rounded-lg"
class="absolute top-2 right-2 sm:top-3 sm:right-3 lg:top-5 lg:right-5 text-muted hover:text-primary transition-all duration-200 p-1.5 sm:p-2 hover:bg-tertiary rounded-lg"
@click=${this.handleCancel}
title="Close (Esc)"
aria-label="Close modal"
@ -455,7 +442,7 @@ export class SessionCreateForm extends LitElement {
<div class="p-3 sm:p-4 lg:p-6 overflow-y-auto flex-grow max-h-[65vh] sm:max-h-[75vh] lg:max-h-[80vh]">
<!-- Session Name -->
<div class="mb-2 sm:mb-3 lg:mb-5">
<label class="form-label text-dark-text-muted text-[10px] sm:text-xs lg:text-sm">Session Name (Optional):</label>
<label class="form-label text-muted text-[10px] sm:text-xs lg:text-sm">Session Name (Optional):</label>
<input
type="text"
class="input-field py-1.5 sm:py-2 lg:py-3 text-xs sm:text-sm"
@ -469,7 +456,7 @@ export class SessionCreateForm extends LitElement {
<!-- Command -->
<div class="mb-2 sm:mb-3 lg:mb-5">
<label class="form-label text-dark-text-muted text-[10px] sm:text-xs lg:text-sm">Command:</label>
<label class="form-label text-muted text-[10px] sm:text-xs lg:text-sm">Command:</label>
<input
type="text"
class="input-field py-1.5 sm:py-2 lg:py-3 text-xs sm:text-sm"
@ -483,7 +470,7 @@ export class SessionCreateForm extends LitElement {
<!-- Working Directory -->
<div class="mb-2 sm:mb-3 lg:mb-5">
<label class="form-label text-dark-text-muted text-[10px] sm:text-xs lg:text-sm">Working Directory:</label>
<label class="form-label text-muted text-[10px] sm:text-xs lg:text-sm">Working Directory:</label>
<div class="flex gap-1.5 sm:gap-2">
<input
type="text"
@ -495,7 +482,7 @@ export class SessionCreateForm extends LitElement {
data-testid="working-dir-input"
/>
<button
class="bg-dark-bg-elevated border border-dark-border rounded-lg p-1.5 sm:p-2 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"
class="bg-elevated border border-base rounded-lg p-1.5 sm:p-2 lg:p-3 font-mono text-muted transition-all duration-200 hover:text-primary hover:bg-hover hover:border-primary hover:shadow-sm flex-shrink-0"
@click=${this.handleBrowse}
?disabled=${this.disabled || this.isCreating}
title="Browse directories"
@ -511,23 +498,23 @@ export class SessionCreateForm extends LitElement {
</div>
<!-- Spawn Window Toggle -->
<div class="mb-2 sm:mb-3 lg:mb-5 flex items-center justify-between bg-dark-bg-elevated border border-dark-border rounded-lg p-2 sm:p-3 lg:p-4">
<div class="mb-2 sm:mb-3 lg:mb-5 flex items-center justify-between bg-elevated border border-base rounded-lg p-2 sm:p-3 lg:p-4">
<div class="flex-1 pr-2 sm:pr-3 lg:pr-4">
<span class="text-dark-text text-[10px] sm:text-xs lg:text-sm font-medium">Spawn window</span>
<p class="text-[9px] sm:text-[10px] lg:text-xs text-dark-text-muted mt-0.5 hidden sm:block">Opens native terminal window</p>
<span class="text-primary text-[10px] sm:text-xs lg:text-sm font-medium">Spawn window</span>
<p class="text-[9px] sm:text-[10px] lg:text-xs text-muted mt-0.5 hidden sm:block">Opens native terminal window</p>
</div>
<button
role="switch"
aria-checked="${this.spawnWindow}"
@click=${this.handleSpawnWindowChange}
class="relative inline-flex h-4 w-8 sm:h-5 sm:w-10 lg:h-6 lg: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-primary' : 'bg-dark-border'
class="relative inline-flex h-4 w-8 sm:h-5 sm:w-10 lg:h-6 lg:w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-base ${
this.spawnWindow ? 'bg-primary' : 'bg-border'
}"
?disabled=${this.disabled || this.isCreating}
data-testid="spawn-window-toggle"
>
<span
class="inline-block h-3 w-3 sm:h-4 sm:w-4 lg:h-5 lg:w-5 transform rounded-full bg-white transition-transform ${
class="inline-block h-3 w-3 sm:h-4 sm:w-4 lg:h-5 lg:w-5 transform rounded-full bg-bg-elevated transition-transform ${
this.spawnWindow ? 'translate-x-4 sm:translate-x-5' : 'translate-x-0.5'
}"
></span>
@ -535,10 +522,10 @@ export class SessionCreateForm extends LitElement {
</div>
<!-- Terminal Title Mode -->
<div class="mb-2 sm:mb-4 lg:mb-6 flex items-center justify-between bg-dark-bg-elevated border border-dark-border rounded-lg p-2 sm:p-3 lg:p-4">
<div class="mb-2 sm:mb-4 lg:mb-6 flex items-center justify-between bg-elevated border border-base rounded-lg p-2 sm:p-3 lg:p-4">
<div class="flex-1 pr-2 sm:pr-3 lg:pr-4">
<span class="text-dark-text text-[10px] sm:text-xs lg:text-sm font-medium">Terminal Title Mode</span>
<p class="text-[9px] sm:text-[10px] lg:text-xs text-dark-text-muted mt-0.5 hidden sm:block">
<span class="text-primary text-[10px] sm:text-xs lg:text-sm font-medium">Terminal Title Mode</span>
<p class="text-[9px] sm:text-[10px] lg:text-xs text-muted mt-0.5 hidden sm:block">
${this.getTitleModeDescription()}
</p>
</div>
@ -546,16 +533,16 @@ export class SessionCreateForm extends LitElement {
<select
.value=${this.titleMode}
@change=${this.handleTitleModeChange}
class="bg-dark-bg-secondary border border-dark-border rounded-lg px-1.5 py-1 pr-6 sm:px-2 sm:py-1.5 sm:pr-7 lg:px-3 lg:py-2 lg:pr-8 text-dark-text text-[10px] sm:text-xs lg:text-sm transition-all duration-200 hover:border-primary-hover focus:border-primary focus:outline-none appearance-none cursor-pointer"
class="bg-secondary border border-base rounded-lg px-1.5 py-1 pr-6 sm:px-2 sm:py-1.5 sm:pr-7 lg:px-3 lg:py-2 lg:pr-8 text-primary text-[10px] sm:text-xs lg:text-sm transition-all duration-200 hover:border-primary-hover focus:border-primary focus:outline-none appearance-none cursor-pointer"
style="min-width: 80px"
?disabled=${this.disabled || this.isCreating}
>
<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-dark-bg-secondary text-dark-text" ?selected=${this.titleMode === TitleMode.FILTER}>Filter</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-dark-bg-secondary text-dark-text" ?selected=${this.titleMode === TitleMode.DYNAMIC}>Dynamic</option>
<option value="${TitleMode.NONE}" class="bg-secondary text-primary" ?selected=${this.titleMode === TitleMode.NONE}>None</option>
<option value="${TitleMode.FILTER}" class="bg-secondary text-primary" ?selected=${this.titleMode === TitleMode.FILTER}>Filter</option>
<option value="${TitleMode.STATIC}" class="bg-secondary text-primary" ?selected=${this.titleMode === TitleMode.STATIC}>Static</option>
<option value="${TitleMode.DYNAMIC}" class="bg-secondary text-primary" ?selected=${this.titleMode === TitleMode.DYNAMIC}>Dynamic</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-1 sm:px-1.5 lg:px-2 text-dark-text-muted">
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-1 sm:px-1.5 lg:px-2 text-muted">
<svg class="h-2.5 w-2.5 sm:h-3 sm:w-3 lg:h-4 lg:w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
@ -565,7 +552,7 @@ export class SessionCreateForm extends LitElement {
<!-- Quick Start Section -->
<div class="mb-2 sm:mb-4 lg:mb-6">
<label class="form-label text-dark-text-muted uppercase text-[9px] sm:text-[10px] lg:text-xs tracking-wider mb-1 sm:mb-2 lg:mb-3"
<label class="form-label text-muted uppercase text-[9px] sm:text-[10px] lg:text-xs tracking-wider mb-1 sm:mb-2 lg:mb-3"
>Quick Start</label
>
<div class="grid grid-cols-2 gap-2 sm:gap-2.5 lg:gap-3 mt-1.5 sm:mt-2">
@ -576,7 +563,7 @@ export class SessionCreateForm extends LitElement {
class="${
this.command === command
? 'px-2 py-1.5 sm:px-3 sm:py-2 lg:px-4 lg:py-3 rounded-lg border text-left transition-all bg-primary bg-opacity-10 border-primary text-primary hover:bg-opacity-20 font-medium text-[10px] sm:text-xs lg:text-sm'
: 'px-2 py-1.5 sm:px-3 sm:py-2 lg:px-4 lg: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 text-[10px] sm:text-xs lg:text-sm'
: 'px-2 py-1.5 sm:px-3 sm:py-2 lg:px-4 lg:py-3 rounded-lg border text-left transition-all bg-elevated border-base text-primary hover:bg-hover hover:border-primary hover:text-primary text-[10px] sm:text-xs lg:text-sm'
}"
?disabled=${this.disabled || this.isCreating}
>
@ -591,14 +578,14 @@ export class SessionCreateForm extends LitElement {
<div class="flex gap-1.5 sm:gap-2 lg:gap-3 mt-2 sm:mt-3 lg:mt-4 xl:mt-6">
<button
class="flex-1 bg-dark-bg-elevated border border-dark-border text-dark-text px-2 py-1 sm:px-3 sm:py-1.5 lg:px-4 lg:py-2 xl:px-6 xl:py-3 rounded-lg font-mono text-[10px] sm:text-xs lg:text-sm transition-all duration-200 hover:bg-dark-surface-hover hover:border-dark-border-light"
class="flex-1 bg-elevated border border-base text-primary px-2 py-1 sm:px-3 sm:py-1.5 lg:px-4 lg:py-2 xl:px-6 xl:py-3 rounded-lg font-mono text-[10px] sm:text-xs lg:text-sm transition-all duration-200 hover:bg-hover hover:border-light"
@click=${this.handleCancel}
?disabled=${this.isCreating}
>
Cancel
</button>
<button
class="flex-1 bg-primary text-black px-2 py-1 sm:px-3 sm:py-1.5 lg:px-4 lg:py-2 xl:px-6 xl:py-3 rounded-lg font-mono text-[10px] sm:text-xs lg:text-sm font-medium transition-all duration-200 hover:bg-primary-hover hover:shadow-glow disabled:opacity-50 disabled:cursor-not-allowed"
class="flex-1 bg-primary text-text-bright px-2 py-1 sm:px-3 sm:py-1.5 lg:px-4 lg:py-2 xl:px-6 xl:py-3 rounded-lg font-mono text-[10px] sm:text-xs lg: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}
?disabled=${
this.disabled ||

View file

@ -265,70 +265,70 @@ export class SessionList extends LitElement {
${
!hasRunningSessions && (!hasExitedSessions || this.hideExited)
? html`
<div class="text-dark-text-muted text-center py-8">
<div class="text-text-muted text-center py-8">
${
this.loading
? 'Loading sessions...'
: this.hideExited && this.sessions.length > 0
? html`
<div class="space-y-4 max-w-2xl mx-auto text-left">
<div class="text-lg font-semibold text-dark-text">
<div class="text-lg font-semibold text-text">
No running sessions
</div>
<div class="text-sm text-dark-text-muted">
<div class="text-sm text-text-muted">
There are exited sessions. Show them by toggling "Hide exited" above.
</div>
</div>
`
: html`
<div class="space-y-6 max-w-2xl mx-auto text-left">
<div class="text-lg font-semibold text-dark-text">
<div class="text-lg font-semibold text-text">
No terminal sessions yet!
</div>
<div class="space-y-3">
<div class="text-sm text-dark-text-muted">
<div class="text-sm text-text-muted">
Get started by using the
<code class="bg-dark-bg-secondary px-2 py-1 rounded">vt</code> command
<code class="bg-bg-secondary px-2 py-1 rounded">vt</code> command
in your terminal:
</div>
<div
class="bg-dark-bg-secondary p-4 rounded-lg font-mono text-xs space-y-2"
class="bg-bg-secondary p-4 rounded-lg font-mono text-xs space-y-2"
>
<div class="text-green-400">vt pnpm run dev</div>
<div class="text-dark-text-muted pl-4"># Monitor your dev server</div>
<div class="text-status-success">vt pnpm run dev</div>
<div class="text-text-muted pl-4"># Monitor your dev server</div>
<div class="text-green-400">vt claude --dangerously...</div>
<div class="text-dark-text-muted pl-4">
<div class="text-status-success">vt claude --dangerously...</div>
<div class="text-text-muted pl-4">
# Keep an eye on AI agents
</div>
<div class="text-green-400">vt --shell</div>
<div class="text-dark-text-muted pl-4">
<div class="text-status-success">vt --shell</div>
<div class="text-text-muted pl-4">
# Open an interactive shell
</div>
<div class="text-green-400">vt python train.py</div>
<div class="text-dark-text-muted pl-4">
<div class="text-status-success">vt python train.py</div>
<div class="text-text-muted pl-4">
# Watch long-running scripts
</div>
</div>
</div>
<div class="space-y-3 border-t border-dark-border pt-4">
<div class="text-sm font-semibold text-dark-text">
<div class="space-y-3 border-t border-border pt-4">
<div class="text-sm font-semibold text-text">
Haven't installed the CLI yet?
</div>
<div class="text-sm text-dark-text-muted space-y-1">
<div class="text-sm text-text-muted space-y-1">
<div> Click the VibeTunnel menu bar icon</div>
<div> Go to Settings Advanced Install CLI Tools</div>
</div>
</div>
<div class="text-xs text-dark-text-muted mt-4">
<div class="text-xs text-text-muted mt-4">
Once installed, any command prefixed with
<code class="bg-dark-bg-secondary px-1 rounded">vt</code> will appear
<code class="bg-bg-secondary px-1 rounded">vt</code> will appear
here, accessible from any browser at localhost:4020.
</div>
</div>
@ -342,8 +342,8 @@ export class SessionList extends LitElement {
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 class="text-xs font-semibold text-text-muted uppercase tracking-wider mb-4">
Active <span class="text-text-dim">(${runningSessions.length})</span>
</h3>
<div class="${this.compactMode ? 'space-y-2' : 'session-flex-responsive'} relative">
${repeat(
@ -357,8 +357,8 @@ export class SessionList extends LitElement {
<div
class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer ${
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'
? 'bg-bg-elevated border border-accent-primary shadow-card-hover'
: 'bg-bg-secondary border border-border hover:bg-bg-tertiary hover:border-border-light hover:shadow-card'
}"
@click=${() =>
this.handleSessionSelect({ detail: session } as CustomEvent)}
@ -394,7 +394,7 @@ export class SessionList extends LitElement {
</div>
<!-- Elegant divider line -->
<div class="w-px h-8 bg-gradient-to-b from-transparent via-dark-border to-transparent"></div>
<div class="w-px h-8 bg-gradient-to-b from-transparent via-border to-transparent"></div>
<!-- Session content -->
<div class="flex-1 min-w-0">
@ -402,7 +402,7 @@ export class SessionList extends LitElement {
class="text-sm font-mono truncate ${
session.id === this.selectedSessionId
? 'text-accent-primary font-medium'
: 'text-dark-text group-hover:text-accent-primary transition-colors'
: 'text-text group-hover:text-accent-primary transition-colors'
}"
>
<inline-edit
@ -420,7 +420,7 @@ export class SessionList extends LitElement {
.onSave=${(newName: string) => this.handleRename(session.id, newName)}
></inline-edit>
</div>
<div class="text-xs text-dark-text-muted truncate flex items-center gap-1">
<div class="text-xs text-text-muted truncate flex items-center gap-1">
${(() => {
// Debug logging for activity status
if (session.status === 'running' && session.activityStatus) {
@ -436,7 +436,7 @@ export class SessionList extends LitElement {
<span class="text-status-warning flex-shrink-0">
${session.activityStatus.specificStatus.status}
</span>
<span class="text-dark-text-muted/50">·</span>
<span class="text-text-muted/50">·</span>
<span class="truncate">
${formatPathForDisplay(session.workingDir)}
</span>
@ -458,7 +458,7 @@ export class SessionList extends LitElement {
session.status === 'running' || session.status === 'exited'
? html`
<button
class="btn-ghost text-status-error p-1.5 rounded-md transition-all hover:bg-dark-bg-elevated hover:shadow-sm hover:scale-110"
class="btn-ghost text-status-error p-1.5 rounded-md transition-all hover:bg-elevated hover:shadow-sm hover:scale-110"
@click=${async (e: Event) => {
e.stopPropagation();
// Kill the session
@ -477,13 +477,13 @@ export class SessionList extends LitElement {
`
: ''
}
<div class="text-xs text-dark-text-muted font-mono">
<div class="text-xs text-text-muted font-mono">
${session.startedAt ? formatSessionDuration(session.startedAt) : ''}
</div>
`
: html`
<!-- Desktop: Time that hides on hover -->
<div class="text-xs text-dark-text-muted font-mono transition-opacity group-hover:opacity-0">
<div class="text-xs text-text-muted font-mono transition-opacity group-hover:opacity-0">
${session.startedAt ? formatSessionDuration(session.startedAt) : ''}
</div>
@ -494,7 +494,7 @@ export class SessionList extends LitElement {
session.status === 'running' || session.status === 'exited'
? html`
<button
class="btn-ghost text-status-error p-1.5 rounded-md transition-all hover:bg-dark-bg-elevated hover:shadow-sm hover:scale-110"
class="btn-ghost text-status-error p-1.5 rounded-md transition-all hover:bg-elevated hover:shadow-sm hover:scale-110"
@click=${async (e: Event) => {
e.stopPropagation();
// Kill the session
@ -546,8 +546,8 @@ export class SessionList extends LitElement {
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 class="text-xs font-semibold text-text-muted uppercase tracking-wider mb-4">
Idle <span class="text-text-dim">(${exitedSessions.length})</span>
</h3>
<div class="${this.compactMode ? 'space-y-2' : 'session-flex-responsive'} relative">
${repeat(
@ -561,8 +561,8 @@ export class SessionList extends LitElement {
<div
class="group flex items-center gap-3 p-3 rounded-lg cursor-pointer ${
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'
? 'bg-bg-elevated border border-accent-primary shadow-card-hover'
: 'bg-bg-secondary border border-border hover:bg-bg-tertiary hover:border-border-light hover:shadow-card opacity-75'
}"
@click=${() =>
this.handleSessionSelect({ detail: session } as CustomEvent)}
@ -573,7 +573,7 @@ export class SessionList extends LitElement {
</div>
<!-- Elegant divider line -->
<div class="w-px h-8 bg-gradient-to-b from-transparent via-dark-border to-transparent"></div>
<div class="w-px h-8 bg-gradient-to-b from-transparent via-border to-transparent"></div>
<!-- Session content -->
<div class="flex-1 min-w-0">
@ -581,7 +581,7 @@ export class SessionList extends LitElement {
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'
: 'text-text-muted group-hover:text-text transition-colors'
}"
title="${
session.name ||
@ -597,7 +597,7 @@ export class SessionList extends LitElement {
: session.command)
}
</div>
<div class="text-xs text-dark-text-dim truncate">
<div class="text-xs text-text-dim truncate">
${formatPathForDisplay(session.workingDir)}
</div>
</div>
@ -605,13 +605,13 @@ export class SessionList extends LitElement {
<!-- Right side: duration and close button -->
<div class="relative flex items-center flex-shrink-0 gap-1">
<!-- Session duration -->
<div class="text-xs text-dark-text-dim font-mono">
<div class="text-xs text-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 flex-shrink-0 hover:text-status-warning hover:bg-dark-bg-elevated hover:shadow-sm"
class="btn-ghost text-text-muted p-1.5 rounded-md transition-all flex-shrink-0 hover:text-status-warning hover:bg-bg-elevated hover:shadow-sm"
@click=${async (e: Event) => {
e.stopPropagation();
try {
@ -688,7 +688,7 @@ export class SessionList extends LitElement {
if (exitedSessions.length === 0 && runningSessions.length === 0) return '';
return html`
<div class="sticky bottom-0 border-t border-dark-border bg-dark-bg-secondary p-3 flex flex-wrap gap-2 shadow-lg" style="z-index: ${Z_INDEX.SESSION_LIST_BOTTOM_BAR};">
<div class="sticky bottom-0 border-t border-border bg-bg-secondary p-3 flex flex-wrap gap-2 shadow-lg" style="z-index: ${Z_INDEX.SESSION_LIST_BOTTOM_BAR};">
<!-- Control buttons with consistent styling -->
${
exitedSessions.length > 0
@ -697,8 +697,8 @@ export class SessionList extends LitElement {
<button
class="font-mono text-xs px-4 py-2 rounded-lg border transition-all duration-200 ${
this.hideExited
? '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-accent-primary bg-accent-primary bg-opacity-10 text-accent-primary hover:bg-opacity-20 hover:shadow-glow-primary-sm'
? 'border-border bg-bg-elevated text-text-muted hover:bg-surface-hover hover:text-accent-primary hover:border-accent-primary hover:shadow-sm active:scale-95'
: 'border-accent-primary bg-accent-primary bg-opacity-10 text-accent-primary hover:bg-opacity-20 hover:shadow-glow-primary-sm active:scale-95'
}"
@click=${() =>
this.dispatchEvent(
@ -707,7 +707,7 @@ export class SessionList extends LitElement {
data-testid="${this.hideExited ? 'show-exited-button' : 'hide-exited-button'}"
>
${this.hideExited ? 'Show' : 'Hide'} Exited
<span class="text-dark-text-dim">(${exitedSessions.length})</span>
<span class="text-text-dim">(${exitedSessions.length})</span>
</button>
<!-- Clean Exited button (only when Show Exited is active) -->
@ -715,7 +715,7 @@ export class SessionList extends LitElement {
!this.hideExited
? html`
<button
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"
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 active:scale-95 disabled:opacity-50"
@click=${this.handleCleanupExited}
?disabled=${this.cleaningExited}
data-testid="clean-exited-button"
@ -734,11 +734,11 @@ export class SessionList extends LitElement {
runningSessions.length > 0
? html`
<button
class="font-mono text-xs px-4 py-2 rounded-lg border transition-all duration-200 border-status-error bg-status-error bg-opacity-10 text-status-error hover:bg-opacity-20"
class="font-mono text-xs px-4 py-2 rounded-lg border transition-all duration-200 border-status-error bg-status-error bg-opacity-10 text-status-error hover:bg-opacity-20 hover:shadow-glow-error-sm active:scale-95"
@click=${() => this.dispatchEvent(new CustomEvent('kill-all-sessions'))}
data-testid="kill-all-button"
>
Kill All <span class="text-dark-text-dim">(${runningSessions.length})</span>
Kill All <span class="text-text-dim">(${runningSessions.length})</span>
</button>
`
: ''

View file

@ -1194,10 +1194,10 @@ export class SessionView extends LitElement {
render() {
if (!this.session) {
return html`
<div class="fixed inset-0 bg-dark-bg flex items-center justify-center">
<div class="text-dark-text font-mono text-center">
<div class="fixed inset-0 bg-base flex items-center justify-center">
<div class="text-primary font-mono text-center">
<div class="text-2xl mb-2">${this.loadingAnimationManager.getLoadingText()}</div>
<div class="text-sm text-dark-text-muted">Waiting for session...</div>
<div class="text-sm text-muted">Waiting for session...</div>
</div>
</div>
`;
@ -1212,12 +1212,12 @@ export class SessionView extends LitElement {
box-shadow: none !important;
}
session-view:focus {
outline: 2px solid rgb(16 185 129) !important;
outline: 2px solid rgb(var(--color-primary)) !important;
outline-offset: -2px;
}
</style>
<div
class="flex flex-col bg-dark-bg font-mono relative"
class="flex flex-col bg-base font-mono relative"
style="height: 100vh; height: 100dvh; outline: none !important; box-shadow: none !important;"
>
<!-- Session Header -->
@ -1252,7 +1252,7 @@ export class SessionView extends LitElement {
<!-- Enhanced Terminal Container -->
<div
class="${this.terminalContainerHeight === '100%' ? 'flex-1' : ''} bg-dark-bg overflow-hidden min-h-0 relative ${
class="${this.terminalContainerHeight === '100%' ? 'flex-1' : ''} bg-bg overflow-hidden min-h-0 relative ${
this.session?.status === 'exited' ? 'session-exited opacity-90' : ''
} ${
// Add safe area padding for landscape mode on mobile to handle notch
@ -1266,11 +1266,11 @@ export class SessionView extends LitElement {
? html`
<!-- Enhanced Loading overlay -->
<div
class="absolute inset-0 bg-dark-bg bg-opacity-90 backdrop-filter backdrop-blur-sm flex items-center justify-center z-10 animate-fade-in"
class="absolute inset-0 bg-bg bg-opacity-90 backdrop-filter backdrop-blur-sm flex items-center justify-center z-10 animate-fade-in"
>
<div class="text-dark-text font-mono text-center">
<div class="text-2xl mb-3 text-accent-primary animate-pulse-primary">${this.loadingAnimationManager.getLoadingText()}</div>
<div class="text-sm text-dark-text-muted">Connecting to session...</div>
<div class="text-primary font-mono text-center">
<div class="text-2xl mb-3 text-primary animate-pulse-primary">${this.loadingAnimationManager.getLoadingText()}</div>
<div class="text-sm text-muted">Connecting to session...</div>
</div>
</div>
`
@ -1303,7 +1303,7 @@ export class SessionView extends LitElement {
class="fixed inset-0 flex items-center justify-center pointer-events-none z-[25]"
>
<div
class="bg-dark-bg-elevated border border-status-warning text-status-warning font-medium text-sm tracking-wide px-6 py-3 rounded-lg shadow-elevated animate-scale-in"
class="bg-elevated border border-status-warning text-status-warning font-medium text-sm tracking-wide px-6 py-3 rounded-lg shadow-elevated animate-scale-in"
>
<span class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-status-warning"></span>
@ -1319,7 +1319,7 @@ export class SessionView extends LitElement {
${
this.isMobile && !this.showMobileInput && !this.useDirectKeyboard
? html`
<div class="flex-shrink-0 p-4" style="background: black;">
<div class="flex-shrink-0 p-4 bg-secondary">
<!-- First row: Arrow keys -->
<div class="flex gap-2 mb-2">
<button
@ -1492,21 +1492,21 @@ export class SessionView extends LitElement {
${
this.isDragOver
? html`
<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-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="fixed inset-0 bg-bg bg-opacity-90 backdrop-blur-sm flex items-center justify-center z-50 pointer-events-none animate-fade-in">
<div class="bg-elevated border-2 border-dashed border-primary rounded-xl p-10 text-center max-w-md mx-4 shadow-2xl animate-scale-in">
<div class="relative mb-6">
<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">
<svg class="w-12 h-12 text-dark-bg" fill="currentColor" viewBox="0 0 20 20">
<div class="w-24 h-24 mx-auto bg-gradient-to-br from-primary to-primary-light rounded-full flex items-center justify-center shadow-glow">
<svg class="w-12 h-12 text-base" fill="currentColor" viewBox="0 0 20 20">
<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 class="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-gradient-to-r from-transparent via-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">
<h3 class="text-2xl font-bold text-primary mb-3">Drop files here</h3>
<p class="text-sm 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-dim 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>
<kbd class="px-2 py-1 bg-tertiary border border-base rounded text-primary font-mono text-xs">V</kbd>
<span class="opacity-75">to paste from clipboard</span>
</div>
</div>

View file

@ -45,12 +45,12 @@ export class CtrlAlphaOverlay extends LitElement {
<div
class="font-mono text-sm mx-4 max-w-sm w-full self-center"
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: rgb(var(--color-bg)); border: 1px solid rgb(var(--color-primary)); 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) */"
>
<div class="text-primary text-center mb-2 font-bold">Ctrl + Key</div>
<!-- Help text -->
<div class="text-xs text-dark-text-muted text-center mb-3 opacity-70">
<div class="text-xs text-muted text-center mb-3 opacity-70">
Build sequences like ctrl+c ctrl+c
</div>
@ -58,8 +58,8 @@ export class CtrlAlphaOverlay extends LitElement {
${
this.ctrlSequence.length > 0
? html`
<div class="text-center mb-4 p-2 border border-dark-border rounded bg-dark-bg">
<div class="text-xs text-dark-text-muted mb-1">Current sequence:</div>
<div class="text-center mb-4 p-2 border border-base rounded bg-base">
<div class="text-xs text-muted mb-1">Current sequence:</div>
<div class="text-sm text-primary font-bold">
${this.ctrlSequence.map((letter) => `Ctrl+${letter}`).join(' ')}
</div>
@ -110,7 +110,7 @@ export class CtrlAlphaOverlay extends LitElement {
</div>
<!-- Common shortcuts info -->
<div class="text-xs text-dark-text-muted text-center mb-3">
<div class="text-xs text-muted text-center mb-3">
<div>Common: C=interrupt, X=exit, O=save, W=search</div>
</div>

View file

@ -224,7 +224,7 @@ export class MobileInputOverlay extends LitElement {
<div
class="mobile-input-container font-mono text-sm mx-4 flex flex-col"
style="background: black; border: 1px solid #569cd6; border-radius: 8px; 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: rgb(var(--color-bg)); border: 1px solid rgb(var(--color-primary)); border-radius: 8px; margin-bottom: ${this.keyboardHeight > 0 ? `${this.keyboardHeight + 180}px` : 'calc(env(keyboard-inset-height, 0px) + 180px)'};/* 180px = estimated quick keyboard height (3 rows) */"
@click=${this.handleContainerClick}
>
<!-- Input Area -->
@ -241,7 +241,7 @@ export class MobileInputOverlay extends LitElement {
@compositionstart=${this.handleCompositionStart}
@compositionupdate=${this.handleCompositionUpdate}
@compositionend=${this.handleCompositionEnd}
style="height: 120px; background: black; color: #d4d4d4; border: none; padding: 12px;"
style="height: 120px; background: rgb(var(--color-bg)); color: rgb(var(--color-text)); border: none; padding: 12px;"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
@ -250,7 +250,7 @@ export class MobileInputOverlay extends LitElement {
</div>
<!-- Controls -->
<div class="p-4 flex gap-2" style="border-top: 1px solid #444;">
<div class="p-4 flex gap-2" style="border-top: 1px solid rgb(var(--color-border));">
<button
class="font-mono px-3 py-2 text-xs transition-colors btn-ghost"
@click=${() => this.onCancel?.()}

View file

@ -8,6 +8,7 @@ import { html, LitElement, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { Z_INDEX } from '../../utils/constants.js';
import type { Session } from '../session-list.js';
import type { Theme } from '../theme-toggle-icon.js';
@customElement('mobile-menu')
export class MobileMenu extends LitElement {
@ -24,6 +25,7 @@ export class MobileMenu extends LitElement {
@property({ type: Function }) onScreenshare?: () => void;
@property({ type: Function }) onMaxWidthToggle?: () => void;
@property({ type: Function }) onOpenSettings?: () => void;
@property({ type: String }) currentTheme: Theme = 'system';
@state() private showMenu = false;
@state() private focusedIndex = -1;
@ -48,12 +50,80 @@ export class MobileMenu extends LitElement {
}
}
private handleThemeChange() {
// Cycle through themes: light -> dark -> system
const themes: Theme[] = ['light', 'dark', 'system'];
const currentIndex = themes.indexOf(this.currentTheme);
const nextIndex = (currentIndex + 1) % themes.length;
const newTheme = themes[nextIndex];
// Update theme
this.currentTheme = newTheme;
localStorage.setItem('vibetunnel-theme', newTheme);
// Apply theme
const root = document.documentElement;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
let effectiveTheme: 'light' | 'dark';
if (newTheme === 'system') {
effectiveTheme = mediaQuery.matches ? 'dark' : 'light';
} else {
effectiveTheme = newTheme;
}
root.setAttribute('data-theme', effectiveTheme);
// Update meta theme-color
const metaTheme = document.querySelector('meta[name="theme-color"]');
if (metaTheme) {
metaTheme.setAttribute('content', effectiveTheme === 'dark' ? '#0a0a0a' : '#fafafa');
}
// Dispatch event
this.dispatchEvent(
new CustomEvent('theme-changed', {
detail: { theme: newTheme },
bubbles: true,
composed: true,
})
);
// Close menu
this.showMenu = false;
this.focusedIndex = -1;
}
private getThemeIcon() {
switch (this.currentTheme) {
case 'light':
return html`<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
</svg>`;
case 'dark':
return html`<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
</svg>`;
case 'system':
return html`<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7H5V5h10v7H8.771z" clip-rule="evenodd"/>
</svg>`;
}
}
private getThemeLabel() {
return this.currentTheme.charAt(0).toUpperCase() + this.currentTheme.slice(1);
}
connectedCallback() {
super.connectedCallback();
// Close menu when clicking outside
document.addEventListener('click', this.handleOutsideClick);
// Add keyboard support
document.addEventListener('keydown', this.handleKeyDown);
// Load saved theme preference
const saved = localStorage.getItem('vibetunnel-theme') as Theme | null;
this.currentTheme = saved || 'system';
}
disconnectedCallback() {
@ -137,7 +207,7 @@ export class MobileMenu extends LitElement {
return html`
<div class="relative w-[44px] flex-shrink-0">
<button
class="p-2 ${this.showMenu ? 'text-accent-primary border-accent-primary' : 'text-dark-text border-dark-border'} hover:border-accent-primary hover:text-accent-primary rounded-lg"
class="p-2 ${this.showMenu ? 'text-primary border-primary' : 'text-primary border-base'} hover:border-primary hover:text-primary rounded-lg"
@click=${this.toggleMenu}
@keydown=${this.handleMenuButtonKeyDown}
title="More actions"
@ -157,13 +227,13 @@ export class MobileMenu extends LitElement {
private renderDropdown() {
return html`
<div
class="absolute right-0 top-full mt-2 bg-dark-surface border border-dark-border rounded-lg shadow-xl py-1 min-w-[200px]"
class="absolute right-0 top-full mt-2 bg-surface border border-base rounded-lg shadow-xl py-1 min-w-[200px]"
style="z-index: ${Z_INDEX.WIDTH_SELECTOR_DROPDOWN};"
>
<!-- New Session -->
<button
class="w-full text-left px-4 py-3 text-sm font-mono text-dark-text hover:bg-dark-bg-secondary hover:text-accent-primary flex items-center gap-3 ${this.focusedIndex === 0 ? 'bg-dark-bg-secondary text-accent-primary' : ''}"
class="w-full text-left px-4 py-3 text-sm font-mono text-primary hover:bg-secondary hover:text-primary flex items-center gap-3 ${this.focusedIndex === 0 ? 'bg-secondary text-primary' : ''}"
@click=${() => this.handleAction(this.onCreateSession)}
data-testid="mobile-new-session"
tabindex="${this.showMenu ? '0' : '-1'}"
@ -174,11 +244,11 @@ export class MobileMenu extends LitElement {
New Session
</button>
<div class="border-t border-dark-border my-1"></div>
<div class="border-t border-base my-1"></div>
<!-- File Browser -->
<button
class="w-full text-left px-4 py-3 text-sm font-mono text-dark-text hover:bg-dark-bg-secondary hover:text-accent-primary flex items-center gap-3 ${this.focusedIndex === 1 ? 'bg-dark-bg-secondary text-accent-primary' : ''}"
class="w-full text-left px-4 py-3 text-sm font-mono text-primary hover:bg-secondary hover:text-primary flex items-center gap-3 ${this.focusedIndex === 1 ? 'bg-secondary text-primary' : ''}"
@click=${() => this.handleAction(this.onOpenFileBrowser)}
data-testid="mobile-file-browser"
tabindex="${this.showMenu ? '0' : '-1'}"
@ -191,7 +261,7 @@ export class MobileMenu extends LitElement {
<!-- Screenshare -->
<button
class="w-full text-left px-4 py-3 text-sm font-mono text-dark-text hover:bg-dark-bg-secondary hover:text-accent-primary flex items-center gap-3 ${this.focusedIndex === 2 ? 'bg-dark-bg-secondary text-accent-primary' : ''}"
class="w-full text-left px-4 py-3 text-sm font-mono text-primary hover:bg-secondary hover:text-primary flex items-center gap-3 ${this.focusedIndex === 2 ? 'bg-secondary text-primary' : ''}"
@click=${() => this.handleAction(this.onScreenshare)}
data-testid="mobile-screenshare"
tabindex="${this.showMenu ? '0' : '-1'}"
@ -207,7 +277,7 @@ export class MobileMenu extends LitElement {
<!-- Width Settings -->
<button
class="w-full text-left px-4 py-3 text-sm font-mono text-dark-text hover:bg-dark-bg-secondary hover:text-accent-primary flex items-center gap-3 ${this.focusedIndex === 3 ? 'bg-dark-bg-secondary text-accent-primary' : ''}"
class="w-full text-left px-4 py-3 text-sm font-mono text-primary hover:bg-secondary hover:text-primary flex items-center gap-3 ${this.focusedIndex === 3 ? 'bg-secondary text-primary' : ''}"
@click=${() => this.handleAction(this.onMaxWidthToggle)}
data-testid="mobile-width-settings"
tabindex="${this.showMenu ? '0' : '-1'}"
@ -218,9 +288,20 @@ export class MobileMenu extends LitElement {
Width: ${this.widthLabel}
</button>
<!-- Theme Toggle -->
<button
class="w-full text-left px-4 py-3 text-sm font-mono text-primary hover:bg-secondary hover:text-primary flex items-center gap-3 ${this.focusedIndex === 4 ? 'bg-secondary text-primary' : ''}"
@click=${() => this.handleThemeChange()}
data-testid="mobile-theme-toggle"
tabindex="${this.showMenu ? '0' : '-1'}"
>
${this.getThemeIcon()}
Theme: ${this.getThemeLabel()}
</button>
<!-- Settings -->
<button
class="w-full text-left px-4 py-3 text-sm font-mono text-dark-text hover:bg-dark-bg-secondary hover:text-accent-primary flex items-center gap-3 ${this.focusedIndex === 4 ? 'bg-dark-bg-secondary text-accent-primary' : ''}"
class="w-full text-left px-4 py-3 text-sm font-mono text-primary hover:bg-secondary hover:text-primary flex items-center gap-3 ${this.focusedIndex === 5 ? 'bg-secondary text-primary' : ''}"
@click=${() => this.handleAction(this.onOpenSettings)}
data-testid="mobile-settings"
tabindex="${this.showMenu ? '0' : '-1'}"

View file

@ -15,6 +15,7 @@ import { authClient } from '../../services/auth-client.js';
import { isAIAssistantSession, sendAIPrompt } from '../../utils/ai-sessions.js';
import { createLogger } from '../../utils/logger.js';
import './mobile-menu.js';
import '../theme-toggle-icon.js';
const logger = createLogger('session-header');
@ -45,8 +46,16 @@ export class SessionHeader extends LitElement {
@property({ type: Function }) onFontSizeChange?: (size: number) => void;
@property({ type: Function }) onScreenshare?: () => void;
@property({ type: Function }) onOpenSettings?: () => void;
@property({ type: String }) currentTheme = 'system';
@state() private isHovered = false;
connectedCallback() {
super.connectedCallback();
// Load saved theme preference
const saved = localStorage.getItem('vibetunnel-theme');
this.currentTheme = (saved as 'light' | 'dark' | 'system') || 'system';
}
private getStatusText(): string {
if (!this.session) return '';
if ('active' in this.session && this.session.active === false) {
@ -56,17 +65,17 @@ export class SessionHeader extends LitElement {
}
private getStatusColor(): string {
if (!this.session) return 'text-dark-text-muted';
if (!this.session) return 'text-muted';
if ('active' in this.session && this.session.active === false) {
return 'text-dark-text-muted';
return '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 (!this.session) return 'bg-muted';
if ('active' in this.session && this.session.active === false) {
return 'bg-dark-text-muted';
return 'bg-muted';
}
return this.session.status === 'running' ? 'bg-status-success' : 'bg-status-warning';
}
@ -86,7 +95,7 @@ export class SessionHeader extends LitElement {
return html`
<!-- Enhanced Header with gradient background -->
<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 px-4 py-2 shadow-sm"
class="flex items-center justify-between border-b border-base text-sm min-w-0 bg-gradient-to-r from-secondary to-tertiary px-4 py-2 shadow-sm"
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 overflow-hidden">
@ -95,7 +104,7 @@ export class SessionHeader extends LitElement {
this.showSidebarToggle && this.sidebarCollapsed
? html`
<button
class="bg-dark-bg-elevated border border-dark-border rounded-lg p-2 font-mono 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"
class="bg-elevated border border-base rounded-lg p-2 font-mono text-muted transition-all duration-200 hover:text-primary hover:bg-hover hover:border-primary hover:shadow-sm flex-shrink-0"
@click=${() => this.onSidebarToggle?.()}
title="Show sidebar (⌘B)"
aria-label="Show sidebar"
@ -110,7 +119,7 @@ export class SessionHeader extends LitElement {
<!-- Create Session button (desktop only) -->
<button
class="hidden sm:flex bg-accent-primary bg-opacity-10 border border-accent-primary text-accent-primary rounded-lg p-2 font-mono transition-all duration-200 hover:bg-accent-primary hover:text-dark-bg hover:shadow-glow-primary-sm flex-shrink-0"
class="hidden sm:flex bg-primary bg-opacity-10 border border-primary text-primary rounded-lg p-2 font-mono transition-all duration-200 hover:bg-primary hover:text-base hover:shadow-glow-primary-sm flex-shrink-0"
@click=${() => this.onCreateSession?.()}
title="Create New Session (⌘K)"
data-testid="create-session-button"
@ -136,7 +145,7 @@ export class SessionHeader extends LitElement {
this.showBackButton
? html`
<button
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"
class="bg-elevated border border-base rounded-lg px-3 py-1.5 font-mono text-xs text-muted transition-all duration-200 hover:text-primary hover:bg-hover hover:border-primary hover:shadow-sm flex-shrink-0"
@click=${() => this.onBack?.()}
>
Back
@ -144,8 +153,8 @@ export class SessionHeader extends LitElement {
`
: ''
}
<div class="text-dark-text min-w-0 flex-1 overflow-hidden">
<div class="text-dark-text-bright font-medium text-xs sm:text-sm min-w-0 overflow-hidden">
<div class="text-primary min-w-0 flex-1 overflow-hidden">
<div class="text-bright font-medium text-xs sm:text-sm min-w-0 overflow-hidden">
<div class="grid grid-cols-[1fr_auto] items-center gap-2 min-w-0" @mouseenter=${this.handleMouseEnter} @mouseleave=${this.handleMouseLeave}>
<inline-edit
class="min-w-0"
@ -166,7 +175,7 @@ export class SessionHeader extends LitElement {
isAIAssistantSession(this.session)
? html`
<button
class="bg-transparent border-0 p-0 cursor-pointer transition-opacity duration-200 text-accent-primary magic-button flex-shrink-0 ${this.isHovered ? 'opacity-50 hover:opacity-100' : 'opacity-0'}"
class="bg-transparent border-0 p-0 cursor-pointer transition-opacity duration-200 text-primary magic-button flex-shrink-0 ${this.isHovered ? 'opacity-50 hover:opacity-100' : 'opacity-0'}"
@click=${(e: Event) => {
e.stopPropagation();
this.handleMagicButton();
@ -212,8 +221,16 @@ export class SessionHeader extends LitElement {
<!-- Desktop buttons - hidden on mobile -->
<div class="hidden sm:flex items-center gap-2">
<!-- Theme toggle -->
<theme-toggle-icon
.theme=${this.currentTheme}
@theme-changed=${(e: CustomEvent) => {
this.currentTheme = e.detail.theme;
}}
></theme-toggle-icon>
<button
class="bg-dark-bg-elevated border border-dark-border rounded-lg p-2 font-mono 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"
class="bg-elevated border border-base rounded-lg p-2 font-mono text-muted transition-all duration-200 hover:text-primary hover:bg-hover hover:border-primary hover:shadow-sm flex-shrink-0"
@click=${(e: Event) => {
e.stopPropagation();
this.onOpenFileBrowser?.();
@ -228,7 +245,7 @@ export class SessionHeader extends LitElement {
</svg>
</button>
<button
class="bg-dark-bg-elevated border border-dark-border rounded-lg p-2 font-mono 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"
class="bg-elevated border border-base rounded-lg p-2 font-mono text-muted transition-all duration-200 hover:text-primary hover:bg-hover hover:border-primary hover:shadow-sm flex-shrink-0"
@click=${() => this.onScreenshare?.()}
title="Start Screenshare"
>
@ -240,7 +257,7 @@ export class SessionHeader extends LitElement {
</svg>
</button>
<button
class="bg-dark-bg-elevated border border-dark-border rounded-lg px-3 py-2 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 width-selector-button"
class="bg-elevated border border-base rounded-lg px-3 py-2 font-mono text-xs text-muted transition-all duration-200 hover:text-primary hover:bg-hover hover:border-primary hover:shadow-sm flex-shrink-0 width-selector-button"
@click=${() => this.onMaxWidthToggle?.()}
title="${this.widthTooltip}"
>
@ -259,6 +276,7 @@ export class SessionHeader extends LitElement {
.onMaxWidthToggle=${this.onMaxWidthToggle}
.onOpenSettings=${this.onOpenSettings}
.onCreateSession=${this.onCreateSession}
.currentTheme=${this.currentTheme}
></mobile-menu>
</div>

View file

@ -37,7 +37,7 @@ export class TerminalDimensions extends LitElement {
return html`
<span
class="hidden sm:inline text-dark-text-muted text-xs opacity-60"
class="hidden sm:inline text-muted text-xs opacity-60"
style="font-size: 10px; line-height: 1;"
>
${this.cols}×${this.rows}

View file

@ -60,15 +60,15 @@ export class WidthSelector extends LitElement {
<!-- Width selector modal -->
<div
class="width-selector-container fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-dark-bg-elevated border border-dark-border rounded-lg shadow-elevated min-w-[280px] max-w-[90vw] animate-fade-in"
class="width-selector-container fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-surface border border-border rounded-lg shadow-elevated min-w-[280px] max-w-[90vw] animate-fade-in"
style="z-index: ${Z_INDEX.WIDTH_SELECTOR_DROPDOWN};"
>
<div class="p-4">
<div class="flex items-center justify-between mb-3">
<div class="text-sm font-semibold text-dark-text">Terminal Width</div>
<div class="text-sm font-semibold text-text-bright">Terminal Width</div>
<!-- Close button for mobile -->
<button
class="sm:hidden p-1.5 rounded-md text-dark-text-muted hover:text-dark-text hover:bg-dark-surface-hover transition-all duration-200"
class="sm:hidden p-1.5 rounded-md text-text-muted hover:text-text hover:bg-surface transition-all duration-200"
@click=${() => this.onClose?.()}
aria-label="Close width selector"
>
@ -83,18 +83,18 @@ export class WidthSelector extends LitElement {
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
? 'bg-accent-primary bg-opacity-10 text-accent-primary border border-accent-primary'
: 'text-dark-text hover:bg-dark-surface-hover hover:text-dark-text-bright border border-transparent'
? 'bg-primary bg-opacity-20 text-primary font-semibold border border-primary'
: 'text-text hover:bg-surface hover:text-text-bright border border-transparent'
}"
@click=${() => this.onWidthSelect?.(width.value)}
>
<span class="font-mono font-medium">${width.label}</span>
<span class="text-dark-text-muted text-xs ml-4">${width.description}</span>
<span class="text-text-muted text-xs ml-4">${width.description}</span>
</button>
`
)}
<div class="border-t border-dark-border mt-3 pt-3">
<div class="text-sm font-semibold text-dark-text mb-2">Custom (20-500)</div>
<div class="border-t border-border mt-3 pt-3">
<div class="text-sm font-semibold text-text-bright mb-2">Custom (20-500)</div>
<div class="flex gap-2">
<input
type="number"
@ -105,7 +105,7 @@ export class WidthSelector extends LitElement {
@input=${this.handleCustomWidthInput}
@keydown=${this.handleCustomWidthKeydown}
@click=${(e: Event) => e.stopPropagation()}
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"
class="flex-1 bg-bg-secondary border border-border rounded-md px-3 py-2 text-sm font-mono text-text placeholder:text-text-dim focus:border-primary focus:shadow-glow-sm transition-all"
/>
<button
class="px-4 py-2 rounded-md text-sm font-medium transition-all duration-200
@ -113,8 +113,8 @@ export class WidthSelector extends LitElement {
!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'
? 'bg-bg-secondary border border-border text-text-muted cursor-not-allowed'
: 'bg-primary text-text-bright hover:bg-primary-hover active:scale-95'
}"
@click=${this.handleCustomWidthSubmit}
?disabled=${
@ -127,15 +127,15 @@ export class WidthSelector extends LitElement {
</button>
</div>
</div>
<div class="border-t border-dark-border mt-3 pt-3">
<div class="text-sm font-semibold text-dark-text mb-3">Font Size</div>
<div class="border-t border-border mt-3 pt-3">
<div class="text-sm font-semibold text-text-bright mb-3">Font Size</div>
<div class="flex items-center gap-3">
<button
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'
? 'border-border bg-bg-secondary text-text-muted cursor-not-allowed'
: 'border-border bg-bg-elevated text-text hover:border-primary hover:text-primary active:scale-95'
}"
@click=${() => this.onFontSizeChange?.(this.terminalFontSize - 1)}
?disabled=${this.terminalFontSize <= 8}
@ -144,15 +144,15 @@ export class WidthSelector extends LitElement {
<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>
<span class="font-mono text-lg font-medium text-dark-text min-w-[60px] text-center">
<span class="font-mono text-lg font-medium text-text-bright min-w-[60px] text-center">
${this.terminalFontSize}px
</span>
<button
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'
? 'border-border bg-bg-secondary text-text-muted cursor-not-allowed'
: 'border-border bg-bg-elevated text-text hover:border-primary hover:text-primary active:scale-95'
}"
@click=${() => this.onFontSizeChange?.(this.terminalFontSize + 1)}
?disabled=${this.terminalFontSize >= 32}
@ -165,8 +165,8 @@ export class WidthSelector extends LitElement {
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'
? 'text-text-muted cursor-not-allowed'
: 'text-text-muted hover:text-text hover:bg-surface'
}"
@click=${() => this.onFontSizeChange?.(14)}
?disabled=${this.terminalFontSize === 14}

View file

@ -16,14 +16,14 @@ export class SidebarHeader extends HeaderBase {
return html`
<div
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"
class="app-header sidebar-header bg-gradient-to-r from-bg-secondary to-bg-tertiary border-b border-border px-4 py-2 shadow-sm"
style="padding-top: max(0.625rem, env(safe-area-inset-top));"
>
<!-- Compact layout for sidebar -->
<div class="flex items-center gap-2">
<!-- Toggle button -->
<button
class="p-2 text-dark-text-muted hover:text-dark-text rounded-lg hover:bg-dark-bg-tertiary transition-all duration-200 flex-shrink-0"
class="p-2 text-text-muted hover:text-text rounded-lg hover:bg-bg-tertiary transition-all duration-200 flex-shrink-0"
@click=${() => this.dispatchEvent(new CustomEvent('toggle-sidebar'))}
title="Collapse sidebar (⌘B)"
aria-label="Collapse sidebar"
@ -44,11 +44,11 @@ export class SidebarHeader extends HeaderBase {
<terminal-icon size="20"></terminal-icon>
<div class="min-w-0">
<h1
class="text-sm font-bold text-accent-primary font-mono group-hover:underline truncate"
class="text-sm font-bold text-primary font-mono group-hover:underline truncate"
>
VibeTunnel
</h1>
<p class="text-dark-text-muted text-xs font-mono">
<p class="text-text-muted text-xs font-mono">
${runningSessions.length} ${runningSessions.length === 1 ? 'session' : 'sessions'}
</p>
</div>
@ -58,7 +58,7 @@ export class SidebarHeader extends HeaderBase {
<div class="flex items-center gap-2 flex-shrink-0">
<!-- Create Session button with primary styling -->
<button
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"
class="p-2 text-primary bg-primary bg-opacity-10 border border-primary hover:bg-opacity-20 rounded-md transition-all duration-200 flex-shrink-0"
@click=${this.handleCreateSession}
title="Create New Session (⌘K)"
data-testid="create-session-button"
@ -85,7 +85,7 @@ export class SidebarHeader extends HeaderBase {
return html`
<div class="user-menu-container relative">
<button
class="font-mono text-xs px-2 py-1 text-dark-text-muted hover:text-dark-text rounded border border-dark-border hover:bg-dark-bg-tertiary transition-all duration-200"
class="font-mono text-xs px-2 py-1 text-text-muted hover:text-text rounded border border-border hover:bg-bg-tertiary transition-all duration-200"
@click=${this.toggleUserMenu}
title="User menu"
>
@ -99,15 +99,15 @@ export class SidebarHeader extends HeaderBase {
this.showUserMenu
? html`
<div
class="absolute right-0 top-full mt-1 bg-dark-surface border border-dark-border rounded-lg shadow-lg py-1 z-50 min-w-32"
class="absolute right-0 top-full mt-1 bg-surface border border-border rounded-lg shadow-lg py-1 z-50 min-w-32"
>
<div
class="px-3 py-1.5 text-xs text-dark-text-muted border-b border-dark-border font-mono"
class="px-3 py-1.5 text-xs text-text-muted border-b border-border font-mono"
>
${this.currentUser}
</div>
<button
class="w-full text-left px-3 py-1.5 text-xs font-mono text-status-warning hover:bg-dark-bg-secondary hover:text-status-error"
class="w-full text-left px-3 py-1.5 text-xs font-mono text-status-warning hover:bg-bg-secondary hover:text-status-error"
@click=${this.handleLogout}
>
Logout

View file

@ -146,15 +146,15 @@ export class SSHKeyManager extends LitElement {
<modal-wrapper
.visible=${this.visible}
modalClass=""
contentClass="modal-content modal-positioned bg-dark-bg border border-dark-border rounded-lg p-6 w-full max-w-4xl max-h-[80vh] overflow-y-auto"
contentClass="modal-content modal-positioned bg-bg border border-base rounded-lg p-6 w-full max-w-4xl max-h-[80vh] overflow-y-auto"
ariaLabel="SSH Key Manager"
@close=${this.handleClose}
.closeOnBackdrop=${true}
.closeOnEscape=${true}
>
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-mono text-dark-text">SSH Key Manager</h2>
<button @click=${this.handleClose} class="text-dark-text-muted hover:text-dark-text">
<h2 class="text-xl font-mono text-primary">SSH Key Manager</h2>
<button @click=${this.handleClose} class="text-muted hover:text-primary">
</button>
</div>
@ -162,13 +162,13 @@ export class SSHKeyManager extends LitElement {
${
this.error
? html`
<div class="bg-status-error text-dark-bg px-4 py-2 rounded mb-4 font-mono text-sm">
<div class="bg-status-error text-bg px-4 py-2 rounded mb-4 font-mono text-sm">
${this.error}
<button
@click=${() => {
this.error = '';
}}
class="ml-2 text-dark-bg hover:text-dark-text"
class="ml-2 text-bg hover:text-primary"
>
</button>
@ -180,14 +180,14 @@ export class SSHKeyManager extends LitElement {
this.success
? html`
<div
class="bg-status-success text-dark-bg px-4 py-2 rounded mb-4 font-mono text-sm"
class="bg-status-success text-bg px-4 py-2 rounded mb-4 font-mono text-sm"
>
${this.success}
<button
@click=${() => {
this.success = '';
}}
class="ml-2 text-dark-bg hover:text-dark-text"
class="ml-2 text-bg hover:text-primary"
>
</button>
@ -198,7 +198,7 @@ export class SSHKeyManager extends LitElement {
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-mono text-lg text-dark-text">SSH Keys</h3>
<h3 class="font-mono text-lg text-primary">SSH Keys</h3>
<button
@click=${() => {
this.showAddForm = !this.showAddForm;
@ -215,8 +215,8 @@ export class SSHKeyManager extends LitElement {
? html`
<div class="space-y-6 mb-4">
<!-- Generate New Key Section -->
<div class="bg-dark-surface border border-dark-border rounded p-4">
<h4 class="text-dark-text font-mono text-lg mb-4 flex items-center gap-2">
<div class="bg-surface border border-base rounded p-4">
<h4 class="text-primary font-mono text-lg mb-4 flex items-center gap-2">
🔑 Generate New SSH Key
</h4>
@ -239,7 +239,7 @@ export class SSHKeyManager extends LitElement {
<div>
<label class="form-label">Algorithm</label>
<div
class="input-field bg-dark-bg-secondary text-dark-text-muted cursor-not-allowed"
class="input-field bg-secondary text-muted cursor-not-allowed"
>
Ed25519 (recommended)
</div>
@ -258,7 +258,7 @@ export class SSHKeyManager extends LitElement {
}}
?disabled=${this.loading}
/>
<p class="text-dark-text-muted text-xs mt-1">
<p class="text-muted text-xs mt-1">
💡 Leave empty for unencrypted key. Password is required when using the
key for signing.
</p>
@ -273,8 +273,8 @@ export class SSHKeyManager extends LitElement {
</div>
<!-- Import Existing Key Section -->
<div class="bg-dark-surface border border-dark-border rounded p-4">
<h4 class="text-dark-text font-mono text-lg mb-4 flex items-center gap-2">
<div class="bg-surface border border-base rounded p-4">
<h4 class="text-primary font-mono text-lg mb-4 flex items-center gap-2">
📁 Import Existing SSH Key
</h4>
@ -308,7 +308,7 @@ export class SSHKeyManager extends LitElement {
}}
?disabled=${this.loading}
></textarea>
<p class="text-dark-text-muted text-xs mt-1">
<p class="text-muted text-xs mt-1">
💡 If the key is password-protected, you'll be prompted for the password
when using it for authentication.
</p>
@ -336,26 +336,26 @@ export class SSHKeyManager extends LitElement {
${
this.showInstructions && this.instructionsKeyId
? html`
<div class="bg-dark-surface border border-dark-border rounded p-4 mb-6">
<div class="bg-surface border border-base rounded p-4 mb-6">
<div class="flex items-center justify-between mb-4">
<h4 class="text-dark-text font-mono text-lg">Setup Instructions</h4>
<h4 class="text-primary font-mono text-lg">Setup Instructions</h4>
<button
@click=${() => {
this.showInstructions = false;
}}
class="text-dark-text-muted hover:text-dark-text"
class="text-muted hover:text-primary"
>
</button>
</div>
<div class="space-y-4">
<div class="bg-dark-bg border border-dark-border rounded p-3">
<p class="text-dark-text-muted text-xs mb-2">
<div class="bg-bg border border-base rounded p-3">
<p class="text-muted text-xs mb-2">
1. Add the public key to your authorized_keys file:
</p>
<div class="relative">
<pre
class="bg-dark-bg-secondary p-2 rounded text-xs overflow-x-auto text-dark-text pr-20"
class="bg-secondary p-2 rounded text-xs overflow-x-auto text-primary pr-20"
>
echo "${this.sshAgent.getPublicKey(this.instructionsKeyId)}" >> ~/.ssh/authorized_keys</pre
>
@ -373,11 +373,11 @@ echo "${this.sshAgent.getPublicKey(this.instructionsKeyId)}" >> ~/.ssh/authorize
</button>
</div>
</div>
<div class="bg-dark-bg border border-dark-border rounded p-3">
<p class="text-dark-text-muted text-xs mb-2">2. Or copy the public key:</p>
<div class="bg-bg border border-base rounded p-3">
<p class="text-muted text-xs mb-2">2. Or copy the public key:</p>
<div class="relative">
<pre
class="bg-dark-bg-secondary p-2 rounded text-xs overflow-x-auto text-dark-text pr-20"
class="bg-secondary p-2 rounded text-xs overflow-x-auto text-primary pr-20"
>
${this.sshAgent.getPublicKey(this.instructionsKeyId)}</pre
>
@ -396,7 +396,7 @@ ${this.sshAgent.getPublicKey(this.instructionsKeyId)}</pre
</button>
</div>
</div>
<p class="text-dark-text-muted text-xs font-mono">
<p class="text-muted text-xs font-mono">
💡 Tip: Make sure ~/.ssh/authorized_keys has correct permissions (600)
</p>
</div>
@ -410,7 +410,7 @@ ${this.sshAgent.getPublicKey(this.instructionsKeyId)}</pre
${
this.keys.length === 0
? html`
<div class="text-center py-8 text-dark-text-muted">
<div class="text-center py-8 text-muted">
<p class="font-mono text-lg mb-2">No SSH keys found</p>
<p class="text-sm">Generate or import a key to get started</p>
</div>
@ -421,7 +421,7 @@ ${this.sshAgent.getPublicKey(this.instructionsKeyId)}</pre
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<h4 class="font-mono font-semibold text-dark-text">${key.name}</h4>
<h4 class="font-mono font-semibold text-primary">${key.name}</h4>
<span class="badge badge-ed25519">${key.algorithm}</span>
${
key.encrypted
@ -429,7 +429,7 @@ ${this.sshAgent.getPublicKey(this.instructionsKeyId)}</pre
: ''
}
</div>
<div class="text-sm text-dark-text-muted font-mono space-y-1">
<div class="text-sm text-muted font-mono space-y-1">
<div>ID: ${key.id}</div>
<div>Fingerprint: ${key.fingerprint}</div>
<div>Created: ${new Date(key.createdAt).toLocaleString()}</div>
@ -445,7 +445,7 @@ ${this.sshAgent.getPublicKey(this.instructionsKeyId)}</pre
</button>
<button
@click=${() => this.handleRemoveKey(key.id, key.name)}
class="btn-ghost text-xs text-status-error hover:bg-status-error hover:text-dark-bg"
class="btn-ghost text-xs text-status-error hover:bg-status-error hover:text-bg"
title="Remove Key"
>
🗑

View file

@ -21,9 +21,9 @@ export class TerminalIcon extends LitElement {
.terminal-icon {
border-radius: 20%;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.3),
0 1px 3px rgba(0, 0, 0, 0.2);
background: rgba(255, 255, 255, 0.05);
0 2px 8px rgb(var(--color-bg-base) / 0.3),
0 1px 3px rgb(var(--color-bg-base) / 0.2);
background: rgb(var(--color-text-bright) / 0.05);
padding: 2px;
}
`;

View file

@ -272,14 +272,14 @@ export class TerminalQuickKeys extends LitElement {
/* The actual bar with buttons */
.quick-keys-bar {
background: #0a0a0a;
border-top: 1px solid #2a2a2a;
background: rgb(var(--color-bg-base));
border-top: 1px solid rgb(var(--color-border-base));
padding: 0.5rem 0.25rem;
/* Prevent iOS from adding its own styling */
-webkit-appearance: none;
appearance: none;
/* Add shadow for visibility */
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.5);
box-shadow: 0 -2px 10px rgb(var(--color-bg-base) / 0.5);
}
/* Quick key buttons */
@ -294,23 +294,23 @@ export class TerminalQuickKeys extends LitElement {
/* Modifier key styling */
.modifier-key {
background-color: #141414;
border-color: #444;
background-color: rgb(var(--color-bg-tertiary));
border-color: rgb(var(--color-border-base));
}
.modifier-key:hover {
background-color: #1f1f1f;
background-color: rgb(var(--color-bg-secondary));
}
/* Active modifier styling */
.modifier-key.active {
background-color: rgb(59, 130, 246);
border-color: rgb(59, 130, 246);
color: white;
background-color: rgb(var(--color-primary));
border-color: rgb(var(--color-primary));
color: rgb(var(--color-text-bright));
}
.modifier-key.active:hover {
background-color: rgb(37, 99, 235);
background-color: rgb(var(--color-primary-hover));
}
/* Arrow key styling */
@ -321,23 +321,23 @@ export class TerminalQuickKeys extends LitElement {
/* Combo key styling (like ^C, ^Z) */
.combo-key {
background-color: #141414;
border-color: #555;
background-color: rgb(var(--color-bg-tertiary));
border-color: rgb(var(--color-border-accent));
}
.combo-key:hover {
background-color: #1f1f1f;
background-color: rgb(var(--color-bg-secondary));
}
/* Special key styling (like ABC) */
.special-key {
background-color: rgb(16, 185, 129);
border-color: rgb(16, 185, 129);
color: white;
background-color: rgb(var(--color-primary));
border-color: rgb(var(--color-primary));
color: rgb(var(--color-text-bright));
}
.special-key:hover {
background-color: rgb(5, 150, 105);
background-color: rgb(var(--color-primary-hover));
}
/* Function key styling */
@ -351,22 +351,22 @@ export class TerminalQuickKeys extends LitElement {
/* Toggle button styling */
.toggle-key {
background-color: #1f1f1f;
border-color: #666;
background-color: rgb(var(--color-bg-secondary));
border-color: rgb(var(--color-border-accent));
}
.toggle-key:hover {
background-color: #2a2a2a;
background-color: rgb(var(--color-bg-tertiary));
}
.toggle-key.active {
background-color: rgb(16, 185, 129);
border-color: rgb(16, 185, 129);
color: white;
background-color: rgb(var(--color-primary));
border-color: rgb(var(--color-primary));
color: rgb(var(--color-text-bright));
}
.toggle-key.active:hover {
background-color: rgb(5, 150, 105);
background-color: rgb(var(--color-primary-hover));
}
/* Ctrl shortcut button styling */
@ -427,7 +427,7 @@ export class TerminalQuickKeys extends LitElement {
<button
type="button"
tabindex="-1"
class="quick-key-btn flex-1 min-w-0 px-0.5 ${this.isLandscape ? 'py-1' : '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 ${modifier ? 'modifier-key' : ''} ${arrow ? 'arrow-key' : ''} ${toggle ? 'toggle-key' : ''} ${toggle && ((key === 'CtrlExpand' && this.showCtrlKeys) || (key === 'F' && this.showFunctionKeys)) ? 'active' : ''} ${modifier && key === 'Option' && this.activeModifiers.has('Option') ? 'active' : ''}"
class="quick-key-btn flex-1 min-w-0 px-0.5 ${this.isLandscape ? 'py-1' : 'py-1.5'} bg-tertiary text-primary text-xs font-mono rounded border border-base hover:bg-surface hover:border-primary transition-all whitespace-nowrap ${modifier ? 'modifier-key' : ''} ${arrow ? 'arrow-key' : ''} ${toggle ? 'toggle-key' : ''} ${toggle && ((key === 'CtrlExpand' && this.showCtrlKeys) || (key === 'F' && this.showFunctionKeys)) ? 'active' : ''} ${modifier && key === 'Option' && this.activeModifiers.has('Option') ? 'active' : ''}"
@mousedown=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
@ -479,7 +479,7 @@ export class TerminalQuickKeys extends LitElement {
<button
type="button"
tabindex="-1"
class="ctrl-shortcut-btn flex-1 min-w-0 px-0.5 ${this.isLandscape ? 'py-1' : '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 ${combo ? 'combo-key' : ''} ${special ? 'special-key' : ''}"
class="ctrl-shortcut-btn flex-1 min-w-0 px-0.5 ${this.isLandscape ? 'py-1' : 'py-1.5'} bg-tertiary text-primary text-xs font-mono rounded border border-base hover:bg-surface hover:border-primary transition-all whitespace-nowrap ${combo ? 'combo-key' : ''} ${special ? 'special-key' : ''}"
@mousedown=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
@ -514,7 +514,7 @@ export class TerminalQuickKeys extends LitElement {
<button
type="button"
tabindex="-1"
class="func-key-btn flex-1 min-w-0 px-0.5 ${this.isLandscape ? 'py-1' : '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"
class="func-key-btn flex-1 min-w-0 px-0.5 ${this.isLandscape ? 'py-1' : 'py-1.5'} bg-tertiary text-primary text-xs font-mono rounded border border-base hover:bg-surface hover:border-primary transition-all whitespace-nowrap"
@mousedown=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
@ -548,7 +548,7 @@ export class TerminalQuickKeys extends LitElement {
<button
type="button"
tabindex="-1"
class="quick-key-btn flex-1 min-w-0 px-0.5 ${this.isLandscape ? 'py-1' : '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 ${modifier ? 'modifier-key' : ''} ${combo ? 'combo-key' : ''} ${special ? 'special-key' : ''} ${toggle ? 'toggle-key' : ''} ${toggle && this.showFunctionKeys ? 'active' : ''}"
class="quick-key-btn flex-1 min-w-0 px-0.5 ${this.isLandscape ? 'py-1' : 'py-1.5'} bg-tertiary text-primary text-xs font-mono rounded border border-base hover:bg-surface hover:border-primary transition-all whitespace-nowrap ${modifier ? 'modifier-key' : ''} ${combo ? 'combo-key' : ''} ${special ? 'special-key' : ''} ${toggle ? 'toggle-key' : ''} ${toggle && this.showFunctionKeys ? 'active' : ''}"
@mousedown=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
@ -583,7 +583,7 @@ export class TerminalQuickKeys extends LitElement {
<button
type="button"
tabindex="-1"
class="quick-key-btn flex-1 min-w-0 px-0.5 ${this.isLandscape ? 'py-0.5' : 'py-1'} 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 ${modifier ? 'modifier-key' : ''} ${combo ? 'combo-key' : ''} ${special ? 'special-key' : ''} ${modifier && key === 'Option' && this.activeModifiers.has('Option') ? 'active' : ''}"
class="quick-key-btn flex-1 min-w-0 px-0.5 ${this.isLandscape ? 'py-0.5' : 'py-1'} bg-tertiary text-primary text-xs font-mono rounded border border-base hover:bg-surface hover:border-primary transition-all whitespace-nowrap ${modifier ? 'modifier-key' : ''} ${combo ? 'combo-key' : ''} ${special ? 'special-key' : ''} ${modifier && key === 'Option' && this.activeModifiers.has('Option') ? 'active' : ''}"
@mousedown=${(e: Event) => {
e.preventDefault();
e.stopPropagation();

View file

@ -135,12 +135,26 @@ export class Terminal extends LitElement {
}
}
private themeObserver?: MutationObserver;
connectedCallback() {
super.connectedCallback();
// Check for debug mode
this.debugMode = new URLSearchParams(window.location.search).has('debug');
// Watch for theme changes
this.themeObserver = new MutationObserver(() => {
if (this.terminal) {
this.terminal.options.theme = this.getTerminalTheme();
}
});
this.themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
// Restore user override preference if we have a sessionId
if (this.sessionId) {
try {
@ -207,6 +221,9 @@ export class Terminal extends LitElement {
disconnectedCallback() {
this.cleanup();
if (this.themeObserver) {
this.themeObserver.disconnect();
}
super.disconnectedCallback();
}
@ -335,6 +352,67 @@ export class Terminal extends LitElement {
return changed;
}
private getTerminalTheme() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
if (isDark) {
// Dark theme (original colors)
return {
background: '#1e1e1e',
foreground: '#d4d4d4',
cursor: 'rgb(var(--color-primary))',
cursorAccent: '#1e1e1e',
// Standard 16 colors (0-15) - using proper xterm colors
black: '#000000',
red: '#cd0000',
green: '#00cd00',
yellow: '#cdcd00',
blue: '#0000ee',
magenta: '#cd00cd',
cyan: '#00cdcd',
white: '#e5e5e5',
brightBlack: '#7f7f7f',
brightRed: '#ff0000',
brightGreen: '#00ff00',
brightYellow: '#ffff00',
brightBlue: '#5c5cff',
brightMagenta: '#ff00ff',
brightCyan: '#00ffff',
brightWhite: '#ffffff',
};
} else {
// Light theme - optimized for readability with softer contrast
return {
background: '#f8f9fa', // Slightly off-white for less eye strain
foreground: '#1f2328', // Dark gray for better readability than pure black
cursor: 'rgb(var(--color-primary))',
cursorAccent: '#f8f9fa',
// Standard 16 colors optimized for light backgrounds
// Based on GitHub light theme for proven readability
black: '#24292f',
red: '#cf222e',
green: '#1a7f37',
yellow: '#9a6700',
blue: '#0969da',
magenta: '#8250df',
cyan: '#1b7c83',
white: '#6e7781',
brightBlack: '#57606a',
brightRed: '#da3633',
brightGreen: '#2da44e',
brightYellow: '#bf8700',
brightBlue: '#218bff',
brightMagenta: '#a475f9',
brightCyan: '#3192aa',
brightWhite: '#8c959f',
// Selection colors for better visibility
selectionBackground: '#0969da',
selectionForeground: '#ffffff',
selectionInactiveBackground: '#e1e4e8',
};
}
}
private async initializeTerminal() {
try {
this.requestUpdate();
@ -399,29 +477,7 @@ export class Terminal extends LitElement {
altClickMovesCursor: true,
rightClickSelectsWord: false,
wordSeparator: ' ()[]{}\'"`',
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4',
cursor: '#10B981',
cursorAccent: '#1e1e1e',
// Standard 16 colors (0-15) - using proper xterm colors
black: '#000000',
red: '#cd0000',
green: '#00cd00',
yellow: '#cdcd00',
blue: '#0000ee',
magenta: '#cd00cd',
cyan: '#00cdcd',
white: '#e5e5e5',
brightBlack: '#7f7f7f',
brightRed: '#ff0000',
brightGreen: '#00ff00',
brightYellow: '#ffff00',
brightBlue: '#5c5cff',
brightMagenta: '#ff00ff',
brightCyan: '#00ffff',
brightWhite: '#ffffff',
},
theme: this.getTerminalTheme(),
});
// Set terminal size - don't call .open() to keep it headless
@ -1133,7 +1189,7 @@ export class Terminal extends LitElement {
// Apply cursor styling after inverse to ensure it takes precedence
if (isCursor) {
style += `background-color: #10B981;`;
style += `background-color: rgb(var(--color-primary));`;
}
// Handle invisible text

View file

@ -0,0 +1,136 @@
import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
export type Theme = 'light' | 'dark' | 'system';
/**
* Icon-only theme toggle button component
* Cycles through light -> dark -> system themes
*/
@customElement('theme-toggle-icon')
export class ThemeToggleIcon extends LitElement {
@property({ type: String })
theme: Theme = 'system';
private readonly STORAGE_KEY = 'vibetunnel-theme';
private mediaQuery?: MediaQueryList;
// Disable shadow DOM to use Tailwind
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
// Load saved theme preference
const saved = localStorage.getItem(this.STORAGE_KEY) as Theme | null;
this.theme = saved || 'system';
// Set up system preference listener
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
this.mediaQuery.addEventListener('change', this.handleSystemThemeChange);
// Apply initial theme
this.applyTheme();
}
disconnectedCallback() {
super.disconnectedCallback();
this.mediaQuery?.removeEventListener('change', this.handleSystemThemeChange);
}
private handleSystemThemeChange = () => {
if (this.theme === 'system') {
this.applyTheme();
}
};
private applyTheme() {
const root = document.documentElement;
let effectiveTheme: 'light' | 'dark';
if (this.theme === 'system') {
effectiveTheme = this.mediaQuery?.matches ? 'dark' : 'light';
} else {
effectiveTheme = this.theme;
}
// Set data-theme attribute
root.setAttribute('data-theme', effectiveTheme);
// Update meta theme-color for mobile browsers
const metaTheme = document.querySelector('meta[name="theme-color"]');
if (metaTheme) {
metaTheme.setAttribute('content', effectiveTheme === 'dark' ? '#0a0a0a' : '#fafafa');
}
}
private cycleTheme() {
// Cycle through: light -> dark -> system
const themes: Theme[] = ['light', 'dark', 'system'];
const currentIndex = themes.indexOf(this.theme);
const nextIndex = (currentIndex + 1) % themes.length;
this.theme = themes[nextIndex];
localStorage.setItem(this.STORAGE_KEY, this.theme);
this.applyTheme();
// Dispatch event for other components that might need to react
this.dispatchEvent(
new CustomEvent('theme-changed', {
detail: { theme: this.theme },
bubbles: true,
composed: true,
})
);
}
private getIcon() {
switch (this.theme) {
case 'light':
return html`
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
</svg>
`;
case 'dark':
return html`
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
</svg>
`;
case 'system':
return html`
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7H5V5h10v7H8.771z" clip-rule="evenodd"/>
</svg>
`;
}
}
private getTooltip() {
const current = this.theme.charAt(0).toUpperCase() + this.theme.slice(1);
const next = this.theme === 'light' ? 'Dark' : this.theme === 'dark' ? 'System' : 'Light';
return `Theme: ${current} (click for ${next})`;
}
render() {
return html`
<button
@click=${this.cycleTheme}
class="bg-elevated border border-base rounded-lg p-2 font-mono text-muted transition-all duration-200 hover:text-primary hover:bg-hover hover:border-primary hover:shadow-sm flex-shrink-0"
title="${this.getTooltip()}"
aria-label="Toggle theme"
>
${this.getIcon()}
</button>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'theme-toggle-icon': ThemeToggleIcon;
}
}

View file

@ -0,0 +1,164 @@
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { Z_INDEX } from '../utils/constants';
export type Theme = 'light' | 'dark' | 'system';
@customElement('theme-toggle')
export class ThemeToggle extends LitElement {
@property({ type: String })
theme: Theme = 'system';
@property({ type: Boolean })
expanded = false;
private readonly STORAGE_KEY = 'vibetunnel-theme';
private mediaQuery?: MediaQueryList;
connectedCallback() {
super.connectedCallback();
// Load saved theme preference
const saved = localStorage.getItem(this.STORAGE_KEY) as Theme | null;
this.theme = saved || 'system';
// Set up system preference listener
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
this.mediaQuery.addEventListener('change', this.handleSystemThemeChange);
// Apply initial theme
this.applyTheme();
}
disconnectedCallback() {
super.disconnectedCallback();
this.mediaQuery?.removeEventListener('change', this.handleSystemThemeChange);
}
private handleSystemThemeChange = () => {
if (this.theme === 'system') {
this.applyTheme();
}
};
private applyTheme() {
const root = document.documentElement;
let effectiveTheme: 'light' | 'dark';
if (this.theme === 'system') {
effectiveTheme = this.mediaQuery?.matches ? 'dark' : 'light';
} else {
effectiveTheme = this.theme;
}
// Set data-theme attribute
root.setAttribute('data-theme', effectiveTheme);
// Update meta theme-color for mobile browsers
const metaTheme = document.querySelector('meta[name="theme-color"]');
if (metaTheme) {
metaTheme.setAttribute('content', effectiveTheme === 'dark' ? '#0a0a0a' : '#fafafa');
}
}
private selectTheme(theme: Theme) {
this.theme = theme;
localStorage.setItem(this.STORAGE_KEY, theme);
this.applyTheme();
this.expanded = false;
// Dispatch event for other components that might need to react
this.dispatchEvent(
new CustomEvent('theme-changed', {
detail: { theme },
bubbles: true,
composed: true,
})
);
}
private toggleExpanded() {
this.expanded = !this.expanded;
}
private handleClickOutside = (e: Event) => {
if (!this.contains(e.target as Node)) {
this.expanded = false;
}
};
updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('expanded')) {
if (this.expanded) {
document.addEventListener('click', this.handleClickOutside);
} else {
document.removeEventListener('click', this.handleClickOutside);
}
}
}
static styles = css``;
createRenderRoot() {
return this;
}
render() {
const currentIcon = this.theme === 'light' ? 'sun' : this.theme === 'dark' ? 'moon' : 'laptop';
return html`
<div class="relative">
<button
@click=${this.toggleExpanded}
class="p-2 text-text border border-border hover:border-primary hover:text-primary rounded-lg transition-all duration-200"
aria-label="Theme toggle"
aria-expanded=${this.expanded}
>
<iconify-icon
icon="ph:${currentIcon}"
class="text-xl"
></iconify-icon>
</button>
${
this.expanded
? html`
<div
class="absolute right-0 mt-2 w-36 bg-elevated border border-border rounded-lg shadow-lg overflow-hidden"
style="z-index: ${Z_INDEX.WIDTH_SELECTOR_DROPDOWN}"
>
<button
@click=${() => this.selectTheme('light')}
class="w-full px-4 py-2 text-left hover:bg-tertiary transition-colors flex items-center gap-3 ${this.theme === 'light' ? 'text-primary' : 'text-text'}"
>
<iconify-icon icon="ph:sun" class="text-lg"></iconify-icon>
<span class="text-sm">Light</span>
</button>
<button
@click=${() => this.selectTheme('dark')}
class="w-full px-4 py-2 text-left hover:bg-tertiary transition-colors flex items-center gap-3 ${this.theme === 'dark' ? 'text-primary' : 'text-text'}"
>
<iconify-icon icon="ph:moon" class="text-lg"></iconify-icon>
<span class="text-sm">Dark</span>
</button>
<button
@click=${() => this.selectTheme('system')}
class="w-full px-4 py-2 text-left hover:bg-tertiary transition-colors flex items-center gap-3 ${this.theme === 'system' ? 'text-primary' : 'text-text'}"
>
<iconify-icon icon="ph:laptop" class="text-lg"></iconify-icon>
<span class="text-sm">System</span>
</button>
</div>
`
: ''
}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'theme-toggle': ThemeToggle;
}
}

View file

@ -245,21 +245,21 @@ export class UnifiedSettings extends LitElement {
return html`
<div class="flex items-center space-x-2">
<span class="text-status-success font-mono"></span>
<span class="text-sm text-dark-text">Active</span>
<span class="text-sm text-primary">Active</span>
</div>
`;
} else if (this.permission === 'granted') {
return html`
<div class="flex items-center space-x-2">
<span class="text-status-warning font-mono">!</span>
<span class="text-sm text-dark-text">Not subscribed</span>
<span class="text-sm text-primary">Not subscribed</span>
</div>
`;
} else {
return html`
<div class="flex items-center space-x-2">
<span class="text-status-error font-mono"></span>
<span class="text-sm text-dark-text">Disabled</span>
<span class="text-sm text-primary">Disabled</span>
</div>
`;
}
@ -289,10 +289,10 @@ export class UnifiedSettings extends LitElement {
style="view-transition-name: settings-modal"
>
<!-- Header -->
<div class="p-4 pb-4 border-b border-dark-border relative flex-shrink-0">
<h2 class="text-accent-green text-lg font-bold">Settings</h2>
<div class="p-4 pb-4 border-b border-base relative flex-shrink-0">
<h2 class="text-primary text-lg font-bold">Settings</h2>
<button
class="absolute top-4 right-4 text-dark-text-muted hover:text-dark-text transition-colors p-1"
class="absolute top-4 right-4 text-muted hover:text-primary transition-colors p-1"
@click=${this.handleClose}
title="Close"
aria-label="Close settings"
@ -321,7 +321,7 @@ export class UnifiedSettings extends LitElement {
return html`
<div class="space-y-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-md font-bold text-dark-text">Notifications</h3>
<h3 class="text-md font-bold text-primary">Notifications</h3>
${this.renderSubscriptionStatus()}
</div>
@ -349,10 +349,10 @@ export class UnifiedSettings extends LitElement {
`
: html`
<!-- Main toggle -->
<div class="flex items-center justify-between p-4 bg-dark-bg-tertiary rounded-lg border border-dark-border">
<div class="flex items-center justify-between p-4 bg-tertiary rounded-lg border border-base"
<div class="flex-1">
<label class="text-dark-text font-medium">Enable Notifications</label>
<p class="text-dark-text-muted text-xs mt-1">
<label class="text-primary font-medium">Enable Notifications</label>
<p class="text-muted text-xs mt-1">
Receive alerts for session events
</p>
</div>
@ -361,12 +361,12 @@ export class UnifiedSettings extends LitElement {
aria-checked="${this.isNotificationsEnabled}"
@click=${this.handleToggleNotifications}
?disabled=${this.isLoading}
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 ${
this.isNotificationsEnabled ? 'bg-accent-green' : 'bg-dark-border'
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-base ${
this.isNotificationsEnabled ? 'bg-primary' : 'bg-border'
}"
>
<span
class="inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${
class="inline-block h-5 w-5 transform rounded-full bg-bg-elevated transition-transform ${
this.isNotificationsEnabled ? 'translate-x-5' : 'translate-x-0.5'
}"
></span>
@ -379,8 +379,8 @@ export class UnifiedSettings extends LitElement {
<!-- Notification types -->
<div class="mt-4 space-y-4">
<div>
<h4 class="text-sm font-medium text-dark-text-muted mb-3">Notification Types</h4>
<div class="space-y-2 bg-dark-bg rounded-lg p-3">
<h4 class="text-sm font-medium text-muted mb-3">Notification Types</h4>
<div class="space-y-2 bg-base rounded-lg p-3"
${this.renderNotificationToggle('sessionExit', 'Session Exit', 'When a session terminates')}
${this.renderNotificationToggle('sessionStart', 'Session Start', 'When a new session starts')}
${this.renderNotificationToggle('sessionError', 'Session Errors', 'When errors occur in sessions')}
@ -390,8 +390,8 @@ export class UnifiedSettings extends LitElement {
<!-- Sound and vibration -->
<div>
<h4 class="text-sm font-medium text-dark-text-muted mb-3">Notification Behavior</h4>
<div class="space-y-2 bg-dark-bg rounded-lg p-3">
<h4 class="text-sm font-medium text-muted mb-3">Notification Behavior</h4>
<div class="space-y-2 bg-base rounded-lg p-3"
${this.renderNotificationToggle('soundEnabled', 'Sound', 'Play sound with notifications')}
${this.renderNotificationToggle('vibrationEnabled', 'Vibration', 'Vibrate device with notifications')}
</div>
@ -399,8 +399,8 @@ export class UnifiedSettings extends LitElement {
</div>
<!-- Test button -->
<div class="flex items-center justify-between pt-3 mt-3 border-t border-dark-border">
<p class="text-xs text-dark-text-muted">Test your notification settings</p>
<div class="flex items-center justify-between pt-3 mt-3 border-t border-base">
<p class="text-xs text-muted">Test your notification settings</p>
<button
class="btn-secondary text-xs px-3 py-1.5"
@click=${this.handleTestNotification}
@ -427,19 +427,19 @@ export class UnifiedSettings extends LitElement {
return html`
<div class="flex items-center justify-between py-2">
<div class="flex-1 pr-4">
<label class="text-dark-text text-sm font-medium">${label}</label>
<p class="text-dark-text-muted text-xs">${description}</p>
<label class="text-primary text-sm font-medium">${label}</label>
<p class="text-muted text-xs">${description}</p>
</div>
<button
role="switch"
aria-checked="${this.notificationPreferences[key]}"
@click=${() => this.handleNotificationPreferenceChange(key, !this.notificationPreferences[key])}
class="relative inline-flex h-5 w-9 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 ${
this.notificationPreferences[key] ? 'bg-accent-green' : 'bg-dark-border'
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-base ${
this.notificationPreferences[key] ? 'bg-primary' : 'bg-border'
}"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
class="inline-block h-4 w-4 transform rounded-full bg-bg-elevated transition-transform ${
this.notificationPreferences[key] ? 'translate-x-4' : 'translate-x-0.5'
}"
></span>
@ -451,18 +451,18 @@ export class UnifiedSettings extends LitElement {
private renderAppSettings() {
return html`
<div class="space-y-4">
<h3 class="text-md font-bold text-dark-text mb-3">Application</h3>
<h3 class="text-md font-bold text-primary mb-3">Application</h3>
<!-- Direct keyboard input (Mobile only) -->
${
this.mediaState.isMobile
? html`
<div class="flex items-center justify-between p-4 bg-dark-bg-tertiary rounded-lg border border-dark-border">
<div class="flex items-center justify-between p-4 bg-tertiary rounded-lg border border-base"
<div class="flex-1">
<label class="text-dark-text font-medium">
<label class="text-primary font-medium">
Use Direct Keyboard
</label>
<p class="text-dark-text-muted text-xs mt-1">
<p class="text-muted text-xs mt-1">
Capture keyboard input directly without showing a text field (desktop-like experience)
</p>
</div>
@ -470,12 +470,12 @@ export class UnifiedSettings extends LitElement {
role="switch"
aria-checked="${this.appPreferences.useDirectKeyboard}"
@click=${() => this.handleAppPreferenceChange('useDirectKeyboard', !this.appPreferences.useDirectKeyboard)}
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 ${
this.appPreferences.useDirectKeyboard ? 'bg-accent-green' : 'bg-dark-border'
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-base ${
this.appPreferences.useDirectKeyboard ? 'bg-primary' : 'bg-border'
}"
>
<span
class="inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${
class="inline-block h-5 w-5 transform rounded-full bg-bg-elevated transition-transform ${
this.appPreferences.useDirectKeyboard ? 'translate-x-5' : 'translate-x-0.5'
}"
></span>
@ -486,10 +486,10 @@ export class UnifiedSettings extends LitElement {
}
<!-- Show log link -->
<div class="flex items-center justify-between p-4 bg-dark-bg-tertiary rounded-lg border border-dark-border">
<div class="flex items-center justify-between p-4 bg-tertiary rounded-lg border border-base">
<div class="flex-1">
<label class="text-dark-text font-medium">Show Log Link</label>
<p class="text-dark-text-muted text-xs mt-1">
<label class="text-primary font-medium">Show Log Link</label>
<p class="text-muted text-xs mt-1">
Display log link for debugging
</p>
</div>
@ -497,12 +497,12 @@ export class UnifiedSettings extends LitElement {
role="switch"
aria-checked="${this.appPreferences.showLogLink}"
@click=${() => this.handleAppPreferenceChange('showLogLink', !this.appPreferences.showLogLink)}
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 ${
this.appPreferences.showLogLink ? 'bg-accent-green' : 'bg-dark-border'
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-base ${
this.appPreferences.showLogLink ? 'bg-primary' : 'bg-border'
}"
>
<span
class="inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${
class="inline-block h-5 w-5 transform rounded-full bg-bg-elevated transition-transform ${
this.appPreferences.showLogLink ? 'translate-x-5' : 'translate-x-0.5'
}"
></span>

View file

@ -199,7 +199,7 @@ export class VibeTerminalBuffer extends LitElement {
}
</style>
<div
class="relative w-full h-full overflow-hidden bg-black"
class="relative w-full h-full overflow-hidden bg-bg"
style="view-transition-name: terminal-${this.sessionId}; min-height: 200px;"
>
${

View file

@ -97,13 +97,28 @@ export class AuthClient {
}
// Return generic avatar SVG for non-macOS or when no avatar found
// Get computed theme colors from CSS variables
const computedStyle = getComputedStyle(document.documentElement);
const bgColor = computedStyle
.getPropertyValue('--color-text-dim')
.trim()
.split(' ')
.map((v) => Number.parseInt(v));
const fgColor = computedStyle
.getPropertyValue('--color-text-muted')
.trim()
.split(' ')
.map((v) => Number.parseInt(v));
const bgColorStr = `rgb(${bgColor.join(', ')})`;
const fgColorStr = `rgb(${fgColor.join(', ')})`;
return (
'data:image/svg+xml;base64,' +
btoa(`
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="24" fill="#6B7280"/>
<circle cx="24" cy="18" r="8" fill="#9CA3AF"/>
<path d="M8 38c0-8.837 7.163-16 16-16s16 7.163 16 16" fill="#9CA3AF"/>
<circle cx="24" cy="24" r="24" fill="${bgColorStr}"/>
<circle cx="24" cy="18" r="8" fill="${fgColorStr}"/>
<path d="M8 38c0-8.837 7.163-16 16-16s16 7.163 16 16" fill="${fgColorStr}"/>
</svg>
`)
);

View file

@ -4,6 +4,33 @@
/* CSS Custom Properties for VibeTunnel constants */
:root {
/* Theme Colors - Light Mode (Default) */
--color-bg: 250 250 250; /* #fafafa - Off-white background */
--color-bg-secondary: 245 245 245; /* #f5f5f5 - Subtle gray */
--color-bg-tertiary: 237 237 237; /* #ededed - Card backgrounds */
--color-bg-elevated: 255 255 255; /* #ffffff - Pure white for elevated surfaces */
--color-surface: 245 245 245; /* #f5f5f5 */
--color-surface-hover: 237 237 237; /* #ededed */
--color-border: 226 226 226; /* #e2e2e2 - Subtle gray borders */
--color-border-light: 237 237 237; /* #ededed */
--color-border-focus: 200 200 200; /* #c8c8c8 */
/* Text colors - Light Mode */
--color-text: 23 23 23; /* #171717 - Near black for high contrast */
--color-text-bright: 0 0 0; /* #000000 - Pure black for emphasis */
--color-text-muted: 75 75 75; /* #4b4b4b - Darker muted gray for AAA compliance */
--color-text-dim: 115 115 115; /* #737373 - Darker dim gray for AA compliance */
/* Primary color (same across themes) */
--color-primary: 16 185 129; /* #10B981 - VibeTunnel green */
--color-primary-hover: 5 150 105; /* #059669 */
--color-primary-dark: 4 120 87; /* #047857 */
/* Status colors */
--color-status-error: 239 68 68; /* #EF4444 */
--color-status-warning: 245 158 11; /* #F59E0B */
--color-status-success: 16 185 129; /* #10B981 */
--color-status-info: 59 130 246; /* #3B82F6 */
/* Breakpoints */
--vt-breakpoint-mobile: 768px;
--vt-breakpoint-tablet: 1024px;
@ -31,16 +58,77 @@
--vt-terminal-resize-debounce: 100ms;
}
/* Global dark theme styles */
/* Dark mode theme colors */
[data-theme="dark"] {
--color-bg: 10 10 10; /* #0a0a0a */
--color-bg-secondary: 20 20 20; /* #141414 */
--color-bg-tertiary: 31 31 31; /* #1f1f1f */
--color-bg-elevated: 38 38 38; /* #262626 */
--color-surface: 26 26 26; /* #1a1a1a */
--color-surface-hover: 42 42 42; /* #2a2a2a */
--color-border: 42 42 42; /* #2a2a2a */
--color-border-light: 58 58 58; /* #3a3a3a */
--color-border-focus: 74 74 74; /* #4a4a4a */
/* Text colors - Dark Mode */
--color-text: 228 228 228; /* #e4e4e4 */
--color-text-bright: 255 255 255; /* #ffffff */
--color-text-muted: 163 163 163; /* #a3a3a3 */
--color-text-dim: 115 115 115; /* #737373 */
/* Primary color (same as light mode) */
--color-primary: 16 185 129; /* #10B981 */
--color-primary-hover: 5 150 105; /* #059669 */
--color-primary-dark: 4 120 87; /* #047857 */
/* Status colors (same as light mode) */
--color-status-error: 239 68 68; /* #EF4444 */
--color-status-warning: 245 158 11; /* #F59E0B */
--color-status-success: 16 185 129; /* #10B981 */
--color-status-info: 59 130 246; /* #3B82F6 */
}
/* System preference detection */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg: 10 10 10;
--color-bg-secondary: 20 20 20;
--color-bg-tertiary: 31 31 31;
--color-bg-elevated: 38 38 38;
--color-surface: 26 26 26;
--color-surface-hover: 42 42 42;
--color-border: 42 42 42;
--color-border-light: 58 58 58;
--color-border-focus: 74 74 74;
--color-text: 228 228 228;
--color-text-bright: 255 255 255;
--color-text-muted: 163 163 163;
--color-text-dim: 115 115 115;
--color-primary: 16 185 129;
--color-primary-hover: 5 150 105;
--color-primary-dark: 4 120 87;
--color-status-error: 239 68 68;
--color-status-warning: 245 158 11;
--color-status-success: 16 185 129;
--color-status-info: 59 130 246;
}
}
/* Global theme-aware styles */
@layer base {
html {
@apply bg-bg text-text;
}
body {
@apply bg-black text-dark-text;
@apply bg-bg text-text;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Default focus styles */
:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.3);
box-shadow: 0 0 0 2px rgb(var(--color-primary) / 0.3);
}
/* iOS Safe Area Support */
@ -66,16 +154,16 @@
/* Glowing terminal icon */
.terminal-icon {
@apply text-primary;
filter: drop-shadow(0 0 10px rgba(16, 185, 129, 0.6));
filter: drop-shadow(0 0 10px rgb(var(--color-primary) / 0.6));
}
/* Input fields with unified style */
.input-field {
@apply bg-dark-bg-secondary border border-dark-border rounded-lg px-4 py-3 text-dark-text w-full;
@apply bg-bg-secondary border border-border rounded-lg px-4 py-3 text-text w-full;
@apply transition-all duration-200 ease-in-out;
@apply hover:border-primary/50 focus:border-primary;
@apply focus:shadow-glow-sm focus:outline-none;
@apply text-left placeholder:text-dark-text-muted;
@apply text-left placeholder:text-text-muted;
}
/* Unified button styles */
@ -98,7 +186,7 @@
}
.btn-primary {
@apply btn-md bg-primary text-black;
@apply btn-md bg-primary text-bg;
@apply hover:bg-primary-hover hover:shadow-glow;
@apply active:scale-95;
}
@ -110,15 +198,15 @@
}
.btn-ghost {
@apply btn-md text-dark-text-muted;
@apply hover:text-dark-text hover:bg-dark-bg-tertiary;
@apply btn-md text-text-muted;
@apply hover:text-text hover:bg-bg-tertiary;
}
/* Enhanced card styles */
.card {
@apply bg-dark-bg-secondary border border-dark-border rounded-lg p-0;
@apply bg-bg-secondary border border-border rounded-lg p-0;
@apply transition-all duration-300 ease-out shadow-sm;
@apply hover:border-dark-border-light hover:shadow-lg hover:-translate-y-1;
@apply hover:border-border-light hover:shadow-lg hover:-translate-y-1;
position: relative;
overflow: hidden;
/* Ensure cards have proper z-index for hover */
@ -138,7 +226,7 @@
content: '';
position: absolute;
inset: -2px;
background: radial-gradient(circle at 50% 50%, transparent 60%, rgba(16, 185, 129, 0.1) 100%);
background: radial-gradient(circle at 50% 50%, transparent 60%, rgb(var(--color-primary) / 0.1) 100%);
opacity: 0;
transition: opacity 0.3s ease-out;
pointer-events: none;
@ -150,8 +238,8 @@
}
.card-elevated {
@apply bg-dark-bg-elevated rounded-xl p-6 shadow-card;
@apply hover:shadow-elevated hover:bg-dark-surface-hover;
@apply bg-bg-elevated rounded-xl p-6 shadow-card;
@apply hover:shadow-elevated hover:bg-surface-hover;
@apply transition-all duration-200;
}
@ -178,26 +266,26 @@
/* Quick start buttons */
.quick-start-btn {
@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 bg-bg-tertiary border border-border rounded-lg px-4 py-3 h-12;
@apply transition-all duration-200 ease-in-out text-text-muted;
@apply hover:border-primary hover:text-primary hover:shadow-glow-sm;
@apply active:scale-95;
}
.quick-start-btn.active {
@apply bg-primary text-black border-primary shadow-glow-sm;
@apply bg-primary text-bg border-primary shadow-glow-sm;
}
/* Modal backdrop */
.modal-backdrop {
@apply fixed inset-0 bg-black bg-opacity-80;
@apply fixed inset-0 bg-bg bg-opacity-80;
z-index: 1000;
backdrop-filter: blur(4px);
}
/* Modal content */
.modal-content {
@apply bg-dark-bg-secondary border border-dark-border rounded-xl;
@apply bg-bg-secondary border border-border rounded-xl;
@apply shadow-2xl shadow-black/50;
position: relative;
z-index: 1001;
@ -213,7 +301,7 @@
/* Labels */
.form-label {
@apply text-dark-text-muted text-sm font-medium mb-2 flex items-center gap-2;
@apply text-text-muted text-sm font-medium mb-2 flex items-center gap-2;
}
/* Enhanced responsive session grid layout */
@ -299,7 +387,7 @@
/* Enhanced authentication styles */
.auth-container {
@apply bg-gradient-to-br from-dark-bg to-dark-bg-secondary flex items-center justify-center p-4;
@apply bg-gradient-to-br from-bg to-bg-secondary flex items-center justify-center p-4;
min-height: 100vh;
min-height: 100dvh; /* Dynamic viewport height for mobile */
}
@ -319,36 +407,36 @@
}
.auth-title {
@apply text-3xl font-mono font-bold text-dark-text-bright;
@apply text-3xl font-mono font-bold text-text-bright;
font-family: 'JetBrains Mono', monospace;
letter-spacing: -0.02em;
}
.auth-subtitle {
@apply text-dark-text-muted font-mono text-xs;
@apply text-text-muted font-mono text-xs;
}
.auth-form {
@apply bg-dark-bg-elevated border border-dark-border rounded-xl shadow-card p-0 w-full;
@apply bg-bg-elevated border border-border rounded-xl shadow-card p-0 w-full;
}
.auth-divider {
@apply relative text-center text-dark-text-muted font-mono text-sm;
@apply relative text-center text-text-muted font-mono text-sm;
}
.auth-divider::before {
@apply absolute top-1/2 left-0 w-full h-px bg-dark-border;
@apply absolute top-1/2 left-0 w-full h-px bg-border;
content: '';
transform: translateY(-50%);
}
.auth-divider span {
@apply bg-dark-bg-secondary px-4;
@apply bg-bg-secondary px-4;
}
/* SSH Key Manager styles */
.ssh-key-item {
@apply bg-dark-bg border-0 rounded-lg p-6;
@apply bg-bg border-0 rounded-lg p-6;
@apply transition-all duration-200 ease-in-out;
}
@ -357,11 +445,11 @@
}
.badge-rsa {
@apply bg-blue-500 text-white;
@apply bg-status-info text-bg-elevated;
}
.badge-ed25519 {
@apply bg-purple-500 text-white;
@apply bg-primary text-bg-elevated;
}
}
@ -898,9 +986,20 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
/* Enhanced terminal container styling */
.terminal-container {
color: #e4e4e4;
overflow: hidden;
background-color: #0a0a0a;
}
/* Terminal container theme styling */
[data-theme="dark"] .terminal-container,
:root:not([data-theme="light"]) .terminal-container {
/* Terminal colors are now handled by xterm.js theme */
}
/* Light theme terminal container styling */
[data-theme="light"] .terminal-container {
/* Terminal colors are now handled by xterm.js theme */
border: 1px solid rgb(var(--color-border));
border-radius: 0.5rem;
}
/* Terminal line styling */
@ -955,20 +1054,20 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
/* Enhanced cursor styling */
.terminal-char.cursor {
animation: cursor-blink 1s infinite;
background-color: #10B981;
color: #0a0a0a;
background-color: rgb(var(--color-primary));
color: rgb(10 10 10); /* Always dark text on cursor */
}
@keyframes cursor-blink {
0%,
50% {
opacity: 1;
background-color: #10B981;
background-color: rgb(var(--color-primary));
}
51%,
100% {
opacity: 0.4;
background-color: #10B981;
background-color: rgb(var(--color-primary));
}
}
@ -976,6 +1075,16 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
background-color: transparent !important;
}
/* Light mode terminal styling */
[data-theme="light"] .xterm {
/* Terminal background is now handled by xterm.js theme */
}
[data-theme="light"] .session-card .terminal-container {
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
border: 1px solid rgb(var(--color-border));
}
/* Ensure terminal container has proper size */
#terminal-player,
#interactive-terminal {
@ -986,8 +1095,8 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
/* Enhanced terminal focus indicator */
.terminal-focused {
box-shadow: 0 0 0 2px #10B981, 0 0 20px rgba(16, 185, 129, 0.3);
border-color: #10B981 !important;
box-shadow: 0 0 0 2px rgb(var(--color-primary)), 0 0 20px rgb(var(--color-primary) / 0.3);
border-color: rgb(var(--color-primary)) !important;
transition: all 0.2s ease-out;
}
@ -996,17 +1105,17 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
position: fixed;
top: 10px;
right: 10px;
background: rgba(16, 185, 129, 0.1);
background: rgb(var(--color-primary) / 0.1);
backdrop-filter: blur(8px);
border: 1px solid #10B981;
color: #10B981;
border: 1px solid rgb(var(--color-primary));
color: rgb(var(--color-primary));
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
z-index: 1000;
animation: slideInFromTop 0.3s ease-out;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2);
box-shadow: 0 2px 8px rgb(var(--color-primary) / 0.2);
}
@keyframes slideInFromTop {
@ -1330,8 +1439,8 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
.keyboard-button {
@apply absolute flex items-center justify-center;
@apply w-12 h-12 rounded-md;
@apply bg-dark-bg/20 border border-dark-border/50;
@apply text-dark-text text-2xl;
@apply bg-bg/20 border border-border/50;
@apply text-text text-2xl;
@apply cursor-pointer select-none;
@apply transition-all duration-200;
backdrop-filter: blur(12px) saturate(1.5);
@ -1358,7 +1467,7 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
/* Hover state for both buttons */
.scroll-to-bottom:hover,
.keyboard-button:hover {
@apply bg-dark-bg/40 border-primary text-primary;
@apply bg-bg/40 border-primary text-primary;
@apply -translate-y-0.5 shadow-glow-sm;
backdrop-filter: blur(16px) saturate(1.8);
-webkit-backdrop-filter: blur(16px) saturate(1.8);
@ -1381,7 +1490,7 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
padding: 8px 12px;
font-family: 'Fira Code', monospace;
font-size: 11px;
color: #e4e4e4;
color: rgb(228 228 228); /* Always light for terminal */
user-select: none;
z-index: 10;
line-height: 1.4;
@ -1416,7 +1525,7 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
align-items: center;
justify-content: center;
cursor: pointer;
color: #e4e4e4;
color: rgb(var(--color-text));
font-size: 20px;
transition: all 0.2s ease;
user-select: none;
@ -1426,9 +1535,9 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
.fit-toggle:hover {
background: rgba(26, 26, 26, 1);
border-color: #10B981;
color: #10B981;
color: rgb(var(--color-primary));
transform: translateY(-1px);
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
box-shadow: 0 0 10px rgb(var(--color-primary) / 0.3);
}
.fit-toggle:active {
@ -1437,8 +1546,8 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
.fit-toggle.active {
border-color: #10B981;
color: #10B981;
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
color: rgb(var(--color-primary));
box-shadow: 0 0 10px rgb(var(--color-primary) / 0.3);
}
/* View Transitions */
@ -1500,83 +1609,12 @@ body.initial-session-load .session-flex-responsive > session-card:nth-child(n +
z-index: 1000;
}
/* Custom morph animation from button to modal with spring effect */
@keyframes expand-from-button {
0% {
transform: scale(0)
translate(calc(var(--vt-button-x) - 50vw), calc(var(--vt-button-y) - 50vh));
opacity: 0;
border-radius: 0.5rem;
}
85% {
transform: scale(1.01) translate(0, 0);
opacity: 1;
border-radius: 0.75rem;
}
100% {
transform: scale(1) translate(0, 0);
opacity: 1;
border-radius: 0.75rem;
}
}
@keyframes shrink-to-button {
0% {
transform: scale(1) translate(0, 0);
opacity: 1;
border-radius: 0.75rem;
}
100% {
transform: scale(0)
translate(calc(var(--vt-button-x) - 50vw), calc(var(--vt-button-y) - 50vh));
opacity: 0;
border-radius: 0.5rem;
}
}
/* Apply the custom animations */
::view-transition-new(create-session-modal) {
animation: expand-from-button 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
::view-transition-old(create-session-modal) {
animation: shrink-to-button 0.25s cubic-bezier(0.4, 0, 0.6, 1);
}
/* Hide button during forward transition */
::view-transition-old(create-session-button) {
animation: fade-out 0.15s ease-out;
}
/* Show button during reverse transition */
::view-transition-new(create-session-button) {
animation: fade-in 0.15s ease-out;
}
/* Make button invisible during transition */
.vt-create-button {
contain: layout;
}
/* Ensure smooth backdrop fade */
.modal-backdrop {
animation: fade-in 0.3s ease-out;
}
/* During close transition, fade backdrop out faster */
body:has(::view-transition-old(create-session-modal)) .modal-backdrop {
animation: fade-out 0.25s ease-out;
}
/* Prevent flicker during modal close on mobile Safari */
body.modal-closing .modal-content {
opacity: 0 !important;
pointer-events: none !important;
}
body.modal-closing .modal-backdrop {
opacity: 0 !important;
}
/* Black hole collapse animation for session removal */

View file

@ -15,7 +15,7 @@ export type GitStatus = 'modified' | 'added' | 'deleted' | 'untracked' | 'unchan
export function getFileIcon(fileName: string, type: FileType): TemplateResult {
if (type === 'directory') {
return html`
<svg class="w-5 h-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-5 h-5 text-status-info" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
</svg>
`;
@ -26,216 +26,216 @@ export function getFileIcon(fileName: string, type: FileType): TemplateResult {
// Icon map for different file extensions
const iconMap: Record<string, TemplateResult> = {
// JavaScript/TypeScript
js: html`<svg class="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
js: html`<svg class="w-5 h-5 text-status-warning" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm6 3a1 1 0 011 1v2a1 1 0 11-2 0V9h-.5a.5.5 0 000 1H10a1 1 0 110 2H8.5A2.5 2.5 0 016 9.5V8a1 1 0 011-1h3z"
/>
</svg>`,
mjs: html`<svg class="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
mjs: html`<svg class="w-5 h-5 text-status-warning" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm6 3a1 1 0 011 1v2a1 1 0 11-2 0V9h-.5a.5.5 0 000 1H10a1 1 0 110 2H8.5A2.5 2.5 0 016 9.5V8a1 1 0 011-1h3z"
/>
</svg>`,
cjs: html`<svg class="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
cjs: html`<svg class="w-5 h-5 text-status-warning" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm6 3a1 1 0 011 1v2a1 1 0 11-2 0V9h-.5a.5.5 0 000 1H10a1 1 0 110 2H8.5A2.5 2.5 0 016 9.5V8a1 1 0 011-1h3z"
/>
</svg>`,
ts: html`<svg class="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
ts: html`<svg class="w-5 h-5 text-status-info" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm6 3h4v1h-1v4a1 1 0 11-2 0V8h-1a1 1 0 110-2zM6 7h2v6H6V7z"
/>
</svg>`,
tsx: html`<svg class="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
tsx: html`<svg class="w-5 h-5 text-status-info" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm6 3h4v1h-1v4a1 1 0 11-2 0V8h-1a1 1 0 110-2zM6 7h2v6H6V7z"
/>
</svg>`,
jsx: html`<svg class="w-5 h-5 text-cyan-400" fill="currentColor" viewBox="0 0 20 20">
jsx: html`<svg class="w-5 h-5 text-status-info" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm2 6a2 2 0 114 0 2 2 0 01-4 0zm6-2a2 2 0 104 0 2 2 0 00-4 0z"
/>
</svg>`,
// Web files
html: html`<svg class="w-5 h-5 text-orange-400" fill="currentColor" viewBox="0 0 20 20">
html: html`<svg class="w-5 h-5 text-status-warning" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm1 2h10v2H5V5zm0 4h10v2H5V9zm0 4h6v2H5v-2z"
/>
</svg>`,
htm: html`<svg class="w-5 h-5 text-orange-400" fill="currentColor" viewBox="0 0 20 20">
htm: html`<svg class="w-5 h-5 text-status-warning" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm1 2h10v2H5V5zm0 4h10v2H5V9zm0 4h6v2H5v-2z"
/>
</svg>`,
css: html`<svg class="w-5 h-5 text-pink-400" fill="currentColor" viewBox="0 0 20 20">
css: html`<svg class="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm4 6a2 2 0 100 4 2 2 0 000-4zm4-2a2 2 0 100 4 2 2 0 000-4z"
/>
</svg>`,
scss: html`<svg class="w-5 h-5 text-pink-400" fill="currentColor" viewBox="0 0 20 20">
scss: html`<svg class="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm4 6a2 2 0 100 4 2 2 0 000-4zm4-2a2 2 0 100 4 2 2 0 000-4z"
/>
</svg>`,
sass: html`<svg class="w-5 h-5 text-pink-400" fill="currentColor" viewBox="0 0 20 20">
sass: html`<svg class="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm4 6a2 2 0 100 4 2 2 0 000-4zm4-2a2 2 0 100 4 2 2 0 000-4z"
/>
</svg>`,
less: html`<svg class="w-5 h-5 text-pink-400" fill="currentColor" viewBox="0 0 20 20">
less: html`<svg class="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm4 6a2 2 0 100 4 2 2 0 000-4zm4-2a2 2 0 100 4 2 2 0 000-4z"
/>
</svg>`,
// Config and data files
json: html`<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
json: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
/>
</svg>`,
jsonc: html`<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
jsonc: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
/>
</svg>`,
xml: html`<svg class="w-5 h-5 text-purple-400" fill="currentColor" viewBox="0 0 20 20">
xml: html`<svg class="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
/>
</svg>`,
yaml: html`<svg class="w-5 h-5 text-purple-400" fill="currentColor" viewBox="0 0 20 20">
yaml: html`<svg class="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
/>
</svg>`,
yml: html`<svg class="w-5 h-5 text-purple-400" fill="currentColor" viewBox="0 0 20 20">
yml: html`<svg class="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
/>
</svg>`,
// Documentation
md: html`<svg class="w-5 h-5 text-gray-300" fill="currentColor" viewBox="0 0 20 20">
md: html`<svg class="w-5 h-5 text-text-muted" fill="currentColor" viewBox="0 0 20 20">
<path
d="M2 6a2 2 0 012-2h12a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V6zm2 0v8h12V6H4zm2 2h8v1H6V8zm0 2h8v1H6v-1zm0 2h6v1H6v-1z"
/>
</svg>`,
markdown: html`<svg class="w-5 h-5 text-gray-300" fill="currentColor" viewBox="0 0 20 20">
markdown: html`<svg class="w-5 h-5 text-text-muted" fill="currentColor" viewBox="0 0 20 20">
<path
d="M2 6a2 2 0 012-2h12a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V6zm2 0v8h12V6H4zm2 2h8v1H6V8zm0 2h8v1H6v-1zm0 2h6v1H6v-1z"
/>
</svg>`,
txt: html`<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
txt: html`<svg class="w-5 h-5 text-text-dim" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm0 2h12v10H4V5zm2 2v6h8V7H6zm2 1h4v1H8V8zm0 2h4v1H8v-1z"
/>
</svg>`,
text: html`<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
text: html`<svg class="w-5 h-5 text-text-dim" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm0 2h12v10H4V5zm2 2v6h8V7H6zm2 1h4v1H8V8zm0 2h4v1H8v-1z"
/>
</svg>`,
// Images
png: html`<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
png: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z"
clip-rule="evenodd"
/>
</svg>`,
jpg: html`<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
jpg: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z"
clip-rule="evenodd"
/>
</svg>`,
jpeg: html`<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
jpeg: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z"
clip-rule="evenodd"
/>
</svg>`,
gif: html`<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
gif: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z"
clip-rule="evenodd"
/>
</svg>`,
webp: html`<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
webp: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z"
clip-rule="evenodd"
/>
</svg>`,
bmp: html`<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
bmp: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z"
clip-rule="evenodd"
/>
</svg>`,
svg: html`<svg class="w-5 h-5 text-indigo-400" fill="currentColor" viewBox="0 0 20 20">
svg: html`<svg class="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 20 20">
<path
d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm6 6L8 7l2 2 2-2-2 2 2 2-2-2-2 2 2-2z"
/>
</svg>`,
// Archives
zip: html`<svg class="w-5 h-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
zip: html`<svg class="w-5 h-5 text-status-warning" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
/>
</svg>`,
tar: html`<svg class="w-5 h-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
tar: html`<svg class="w-5 h-5 text-status-warning" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
/>
</svg>`,
gz: html`<svg class="w-5 h-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
gz: html`<svg class="w-5 h-5 text-status-warning" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
/>
</svg>`,
rar: html`<svg class="w-5 h-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
rar: html`<svg class="w-5 h-5 text-status-warning" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
/>
</svg>`,
'7z': html`<svg class="w-5 h-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
'7z': html`<svg class="w-5 h-5 text-status-warning" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
/>
</svg>`,
// Documents
pdf: html`<svg class="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
pdf: html`<svg class="w-5 h-5 text-status-error" fill="currentColor" viewBox="0 0 20 20">
<path d="M4 18h12V6h-4V2H4v16zm8-14v4h4l-4-4zM6 10h8v1H6v-1zm0 2h8v1H6v-1zm0 2h6v1H6v-1z" />
</svg>`,
// Scripts
sh: html`<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
sh: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 000 2h11.586l-2.293 2.293a1 1 0 101.414 1.414L17.414 6H19a1 1 0 100-2H3zM3 11a1 1 0 100 2h3.586l-2.293 2.293a1 1 0 101.414 1.414L9.414 13H11a1 1 0 100-2H3z"
/>
</svg>`,
bash: html`<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
bash: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 000 2h11.586l-2.293 2.293a1 1 0 101.414 1.414L17.414 6H19a1 1 0 100-2H3zM3 11a1 1 0 100 2h3.586l-2.293 2.293a1 1 0 101.414 1.414L9.414 13H11a1 1 0 100-2H3z"
/>
</svg>`,
zsh: html`<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
zsh: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 000 2h11.586l-2.293 2.293a1 1 0 101.414 1.414L17.414 6H19a1 1 0 100-2H3zM3 11a1 1 0 100 2h3.586l-2.293 2.293a1 1 0 101.414 1.414L9.414 13H11a1 1 0 100-2H3z"
/>
</svg>`,
fish: html`<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
fish: html`<svg class="w-5 h-5 text-status-success" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 000 2h11.586l-2.293 2.293a1 1 0 101.414 1.414L17.414 6H19a1 1 0 100-2H3zM3 11a1 1 0 100 2h3.586l-2.293 2.293a1 1 0 101.414 1.414L9.414 13H11a1 1 0 100-2H3z"
/>
@ -251,7 +251,7 @@ export function getFileIcon(fileName: string, type: FileType): TemplateResult {
*/
export function getDefaultFileIcon(): TemplateResult {
return html`
<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-5 h-5 text-text-dim" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z"
@ -266,7 +266,7 @@ export function getDefaultFileIcon(): TemplateResult {
*/
export function getParentDirectoryIcon(): TemplateResult {
return html`
<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-5 h-5 text-text-dim" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
@ -291,10 +291,10 @@ export function renderGitStatusBadge(status?: GitStatus): TemplateResult | strin
};
const colorClasses: Record<GitStatus, string> = {
modified: 'bg-yellow-900/50 text-yellow-400',
added: 'bg-green-900/50 text-green-400',
deleted: 'bg-red-900/50 text-red-400',
untracked: 'bg-gray-700 text-gray-400',
modified: 'bg-status-warning/20 text-status-warning',
added: 'bg-status-success/20 text-status-success',
deleted: 'bg-status-error/20 text-status-error',
untracked: 'bg-text-dim/20 text-text-dim',
unchanged: '',
};
@ -321,7 +321,7 @@ export const UIIcons = {
`,
folder: html`
<svg class="w-6 h-6 text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-6 h-6 text-status-info" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
</svg>
`,
@ -336,7 +336,7 @@ export const UIIcons = {
`,
preview: html`
<svg class="w-16 h-16 mb-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-16 h-16 mb-4 text-text-dim" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z"
@ -346,7 +346,7 @@ export const UIIcons = {
`,
binary: html`
<svg class="w-16 h-16 mb-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-16 h-16 mb-4 text-text-dim" fill="currentColor" viewBox="0 0 20 20">
<path
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"
/>

View file

@ -107,7 +107,20 @@ export async function initializeMonaco(): Promise<void> {
},
});
monaco.editor.setTheme('vs-dark');
// Set initial theme based on current theme
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
monaco.editor.setTheme(isDark ? 'vs-dark' : 'vs');
// Watch for theme changes
const themeObserver = new MutationObserver(() => {
const currentTheme = document.documentElement.getAttribute('data-theme') === 'dark';
monaco.editor.setTheme(currentTheme ? 'vs-dark' : 'vs');
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
// Add custom themes if needed
/*monaco.editor.defineTheme('vibetunnel-dark', {

View file

@ -4,24 +4,24 @@ module.exports = {
theme: {
extend: {
colors: {
// Unified Dark theme colors with consistent depth
"dark-bg": "#0a0a0a",
"dark-bg-secondary": "#141414",
"dark-bg-tertiary": "#1f1f1f",
"dark-bg-elevated": "#262626",
"dark-surface": "#1a1a1a",
"dark-surface-hover": "#2a2a2a",
"dark-border": "#2a2a2a",
"dark-border-light": "#3a3a3a",
"dark-border-focus": "#4a4a4a",
// Theme-aware colors using CSS variables
"bg": "rgb(var(--color-bg) / <alpha-value>)",
"bg-secondary": "rgb(var(--color-bg-secondary) / <alpha-value>)",
"bg-tertiary": "rgb(var(--color-bg-tertiary) / <alpha-value>)",
"bg-elevated": "rgb(var(--color-bg-elevated) / <alpha-value>)",
"surface": "rgb(var(--color-surface) / <alpha-value>)",
"surface-hover": "rgb(var(--color-surface-hover) / <alpha-value>)",
"border": "rgb(var(--color-border) / <alpha-value>)",
"border-light": "rgb(var(--color-border-light) / <alpha-value>)",
"border-focus": "rgb(var(--color-border-focus) / <alpha-value>)",
// Text colors
"dark-text": "#e4e4e4",
"dark-text-bright": "#ffffff",
"dark-text-muted": "#a3a3a3",
"dark-text-dim": "#737373",
"text": "rgb(var(--color-text) / <alpha-value>)",
"text-bright": "rgb(var(--color-text-bright) / <alpha-value>)",
"text-muted": "rgb(var(--color-text-muted) / <alpha-value>)",
"text-dim": "rgb(var(--color-text-dim) / <alpha-value>)",
// Unified accent color - Vibrant teal-green
// Unified accent color - Vibrant teal-green (stays the same across themes)
"primary": "#10B981",
"primary-hover": "#059669",
"primary-dark": "#047857",
@ -34,18 +34,6 @@ module.exports = {
"status-warning": "#F59E0B",
"status-success": "#10B981",
"status-info": "#3B82F6",
// Legacy mappings for gradual migration
"accent-primary": "#10B981",
"accent-primary-dark": "#059669",
"accent-primary-darker": "#047857",
"accent-primary-light": "#34D399",
"accent-primary-glow": "#10B98166",
"accent-green": "#10B981",
"accent-green-dark": "#059669",
"accent-green-darker": "#047857",
"accent-green-light": "#34D399",
"accent-green-glow": "#10B98166",
},
boxShadow: {
// Unified glow effects with primary color
@ -53,20 +41,19 @@ module.exports = {
'glow-sm': '0 0 10px rgba(16, 185, 129, 0.3)',
'glow-lg': '0 0 30px rgba(16, 185, 129, 0.5)',
'glow-intense': '0 0 40px rgba(16, 185, 129, 0.6)',
// Legacy mappings
'glow-primary': '0 0 20px rgba(16, 185, 129, 0.4)',
'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)',
// Status-specific glow effects
'glow-error': '0 0 20px rgba(239, 68, 68, 0.4)',
'glow-error-sm': '0 0 10px rgba(239, 68, 68, 0.3)',
'glow-error-lg': '0 0 30px rgba(239, 68, 68, 0.5)',
'glow-warning': '0 0 20px rgba(245, 158, 11, 0.4)',
'glow-warning-sm': '0 0 10px rgba(245, 158, 11, 0.3)',
'glow-warning-lg': '0 0 30px rgba(245, 158, 11, 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-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)',
},
animation: {
'pulse-green': 'pulseGreen 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'pulse-primary': 'pulsePrimary 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'slide-in-right': 'slideInRight 0.3s ease-out',
'slide-in-bottom': 'slideInBottom 0.3s ease-out',
@ -74,14 +61,6 @@ module.exports = {
'scale-in': 'scaleIn 0.2s ease-out',
},
keyframes: {
pulseGreen: {
'0%, 100%': {
opacity: '1',
},
'50%': {
opacity: '.8',
},
},
pulsePrimary: {
'0%, 100%': {
opacity: '1',