Merge branch 'fix/mobile-terminal-overlay' into dev

This commit is contained in:
Alex Blank
2026-05-29 17:20:22 +02:00
2 changed files with 235 additions and 46 deletions
+86 -30
View File
@@ -5,6 +5,7 @@ import {
TerminalSessionTabs,
type TerminalSessionInfo,
} from "../components/terminal-session-tabs";
import { Icon } from "../components/icon";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
import { useAutoHide } from "../hooks/use-auto-hide";
import { useTerminalSessions } from "../hooks/use-terminal-sessions";
@@ -241,45 +242,99 @@ export const TerminalPage: React.FC = () => {
const sessionInfos = SESSIONS_TO_INFO(sessions);
if (isMobile) {
const activeSession = sessions.find((s) => s.id === activeSessionId);
const status =
terminalStatuses[activeSessionId ?? "default"] ?? "connecting";
return (
<section
className={`terminal-page mobile ${isFullscreen ? "fullscreen" : ""}`}
>
{/* Overlay status bar — floats over terminal, never resizes it */}
<div
className={`terminal-page-header mobile-header ${headerAutoHide.isVisible ? "visible" : "hidden"}`}
onClick={() => headerAutoHide.show()}
className={`mobile-terminal-overlay ${headerAutoHide.isVisible ? "visible" : "hidden"}`}
onClick={(e) => e.stopPropagation()}
>
<button
className="secondary-button"
onClick={() => navigate(-1)}
type="button"
>
Back
</button>
<h1>Terminal</h1>
<button
className="secondary-button"
onClick={() => setIsFullscreen((p) => !p)}
type="button"
>
{isFullscreen ? "Exit" : "Fullscreen"}
</button>
<div className="mobile-terminal-toolbar">
<div className="mobile-terminal-toolbar-left">
<button
className="mobile-terminal-toolbtn"
onClick={() => navigate(-1)}
type="button"
aria-label="Back"
>
<Icon name="arrow-left" size="sm" />
</button>
</div>
<div className="mobile-terminal-toolbar-center">
<span className="mobile-terminal-title">
{activeSession?.name || "Terminal"}
</span>
<span
className={`mobile-terminal-status status-dot ${status}`}
aria-label={`Connection status: ${status}`}
/>
</div>
<div className="mobile-terminal-toolbar-right">
<button
className="mobile-terminal-toolbtn"
onClick={() => handleFontSizeChange(-1)}
type="button"
aria-label="Decrease font size"
>
<span style={{ fontSize: "0.75rem" }}>A-</span>
</button>
<button
className="mobile-terminal-toolbtn"
onClick={() => handleFontSizeChange(1)}
type="button"
aria-label="Increase font size"
>
<span style={{ fontSize: "1rem" }}>A+</span>
</button>
<button
className="mobile-terminal-toolbtn"
onClick={() => navigate(-1)}
type="button"
aria-label="Exit terminal"
>
<Icon name="close" size="sm" />
</button>
</div>
</div>
<div className="mobile-terminal-overlay-tabs">
<TerminalSessionTabs
sessions={sessionInfos}
activeSessionId={activeSessionId ?? ""}
onSelect={handleSelect}
onClose={handleClose}
onCreate={handleCreate}
onRename={handleRename}
isMobile={true}
/>
</div>
</div>
{/* Pull handle — visible when overlay is hidden */}
{!headerAutoHide.isVisible && (
<button
className="mobile-terminal-pull-handle"
onClick={(e) => {
e.stopPropagation();
headerAutoHide.show();
}}
type="button"
aria-label="Show toolbar"
>
<span className="mobile-terminal-pull-dots"></span>
</button>
)}
{/* Terminal content — always fills full viewport */}
<div
className={`mobile-tabs-container ${headerAutoHide.isVisible ? "visible" : "hidden"}`}
onClick={() => headerAutoHide.show()}
className="terminal-page-content mobile-full"
onClick={() => headerAutoHide.hide()}
>
<TerminalSessionTabs
sessions={sessionInfos}
activeSessionId={activeSessionId ?? ""}
onSelect={handleSelect}
onClose={handleClose}
onCreate={handleCreate}
onRename={handleRename}
isMobile={true}
/>
</div>
<div className="terminal-page-content">
{error && <div className="terminal-error-banner">{error}</div>}
{sessions
.filter((session) => session.id === activeSessionId)
@@ -291,6 +346,7 @@ export const TerminalPage: React.FC = () => {
sessionId={session.id}
onClose={() => handleClose(session.id)}
isMobile={true}
showControls={false}
onTerminalReady={handleTerminalReady}
/>
</div>
+149 -16
View File
@@ -2950,42 +2950,175 @@ a.nav-item,
background: #cd3131;
}
/* Mobile auto-hide header and tabs */
.terminal-page.mobile .terminal-page-header,
.mobile-tabs-container {
/* ============================================
Mobile Terminal Overlay
============================================ */
/* Mobile terminal page — no padding, terminal fills viewport */
.terminal-page.mobile {
padding: 0;
gap: 0;
height: 100vh;
height: 100dvh;
position: relative;
overflow: hidden;
}
/* Overlay status bar — floats over terminal, never resizes it */
.mobile-terminal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #2d2d2d;
border-bottom: 1px solid #3e3e3e;
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
.terminal-page.mobile .terminal-page-header.hidden,
.mobile-tabs-container.hidden {
.mobile-terminal-overlay.hidden {
transform: translateY(-100%);
opacity: 0;
pointer-events: none;
}
.terminal-page.mobile .terminal-page-header.visible,
.mobile-tabs-container.visible {
.mobile-terminal-overlay.visible {
transform: translateY(0);
opacity: 1;
}
/* Toolbar row */
.mobile-terminal-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-3);
gap: var(--space-2);
}
.mobile-terminal-toolbar-left,
.mobile-terminal-toolbar-right {
display: flex;
align-items: center;
gap: var(--space-1);
flex: 0 0 auto;
}
.mobile-terminal-toolbar-center {
display: flex;
align-items: center;
gap: var(--space-2);
flex: 1;
justify-content: center;
min-width: 0;
}
.mobile-terminal-title {
font-size: 0.875rem;
font-weight: 500;
color: #d4d4d4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mobile-terminal-status {
width: 8px;
height: 8px;
border-radius: 50%;
background: #666;
flex-shrink: 0;
}
.mobile-terminal-status.connecting {
background: #f5f543;
animation: pulse 1.5s infinite;
}
.mobile-terminal-status.connected {
background: #0dbc79;
}
.mobile-terminal-status.disconnected,
.mobile-terminal-status.error {
background: #cd3131;
}
.mobile-terminal-toolbtn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
background: transparent;
border: 1px solid #3e3e3e;
border-radius: 6px;
color: #d4d4d4;
cursor: pointer;
font-size: 0.875rem;
transition: background 0.2s ease;
}
.mobile-terminal-toolbtn:hover {
background: #3e3e3e;
}
/* Session tabs inside overlay */
.mobile-terminal-overlay-tabs {
background: #1e1e1e;
border-top: 1px solid #3e3e3e;
}
.mobile-terminal-overlay-tabs .terminal-session-tabs {
background: #1e1e1e;
border-bottom: none;
}
/* Pull handle — shown when overlay is hidden */
.mobile-terminal-pull-handle {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
z-index: 99;
padding: 4px 16px;
background: #2d2d2d;
border: 1px solid #3e3e3e;
border-top: none;
border-radius: 0 0 12px 12px;
color: #888;
cursor: pointer;
font-size: 0.75rem;
line-height: 1;
transition: background 0.2s ease;
}
.mobile-terminal-pull-handle:hover {
background: #3e3e3e;
}
.mobile-terminal-pull-dots {
pointer-events: none;
}
/* Terminal content — always fills full viewport on mobile */
.terminal-page-content.mobile-full {
flex: 1;
min-height: 0;
border: none;
border-radius: 0;
overflow: hidden;
}
/* Mobile fullscreen */
@media (max-width: 767px) {
.terminal-page.fullscreen {
padding: 0;
}
.terminal-page.mobile .terminal-page-header {
padding: var(--space-2);
gap: var(--space-2);
}
.terminal-page.mobile .terminal-page-header h1 {
font-size: 1rem;
}
.terminal-session-tab-name {
max-width: 80px;
}