refactor: extract TerminalPage components
- Extract use-terminal-page hook for terminal state and effects - Extract MobileTerminalView and DesktopTerminalView components - Slim TerminalPage from 571 to 112 lines Quality gates: tsc --noEmit passes, npm run build passes
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import React from "react";
|
||||
import { TerminalComponent, type TerminalRef } from "./terminal";
|
||||
import { TerminalSessionTabs, type TerminalSessionInfo } from "./terminal-session-tabs";
|
||||
import type { TerminalSession } from "../../../api/terminal";
|
||||
|
||||
interface Props {
|
||||
instanceId: string;
|
||||
sessions: TerminalSession[];
|
||||
sessionInfos: TerminalSessionInfo[];
|
||||
activeSessionId: string;
|
||||
terminalRefs: React.MutableRefObject<Record<string, React.RefObject<TerminalRef>>>;
|
||||
isFullscreen: boolean;
|
||||
status: string;
|
||||
error: string | null;
|
||||
loading: boolean;
|
||||
showResetConfirm: boolean;
|
||||
onFullscreenClick: (e: React.MouseEvent<HTMLElement>) => void;
|
||||
onSelect: (id: string) => void;
|
||||
onClose: (id: string) => void;
|
||||
onCreate: () => void;
|
||||
onRename: (id: string, name: string) => void;
|
||||
onNavigateBack: () => void;
|
||||
onToggleFullscreen: () => void;
|
||||
onFontSizeChange: (delta: number) => void;
|
||||
onShowResetConfirm: () => void;
|
||||
onHideResetConfirm: () => void;
|
||||
onReset: () => void;
|
||||
onTerminalReady: (
|
||||
sendData: (data: string) => void,
|
||||
status: "connecting" | "connected" | "disconnected" | "error" | "resetting",
|
||||
focusInput: () => void,
|
||||
changeFontSize: (delta: number) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const DesktopTerminalView: React.FC<Props> = ({
|
||||
instanceId,
|
||||
sessions,
|
||||
sessionInfos,
|
||||
activeSessionId,
|
||||
terminalRefs,
|
||||
isFullscreen,
|
||||
status,
|
||||
error,
|
||||
loading,
|
||||
showResetConfirm,
|
||||
onFullscreenClick,
|
||||
onSelect,
|
||||
onClose,
|
||||
onCreate,
|
||||
onRename,
|
||||
onNavigateBack,
|
||||
onToggleFullscreen,
|
||||
onFontSizeChange,
|
||||
onShowResetConfirm,
|
||||
onHideResetConfirm,
|
||||
onReset,
|
||||
onTerminalReady,
|
||||
}) => {
|
||||
return (
|
||||
<section
|
||||
className={`terminal-page ${isFullscreen ? "fullscreen" : ""}`}
|
||||
onClick={onFullscreenClick}
|
||||
>
|
||||
{!isFullscreen && (
|
||||
<div className="terminal-page-header">
|
||||
<button className="secondary-button" onClick={onNavigateBack} type="button">
|
||||
Back
|
||||
</button>
|
||||
<h1>Terminal</h1>
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={onToggleFullscreen}
|
||||
type="button"
|
||||
title="Toggle fullscreen (Alt+Shift+F)"
|
||||
>
|
||||
{isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isFullscreen ? (
|
||||
<div className="terminal-fullscreen-header">
|
||||
<div className="terminal-fullscreen-header-tabs">
|
||||
<TerminalSessionTabs
|
||||
sessions={sessionInfos}
|
||||
activeSessionId={activeSessionId}
|
||||
onSelect={onSelect}
|
||||
onClose={onClose}
|
||||
onCreate={onCreate}
|
||||
onRename={onRename}
|
||||
isMobile={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="terminal-fullscreen-header-controls">
|
||||
<span
|
||||
className={`terminal-fullscreen-status status-dot ${status}`}
|
||||
aria-label={`Terminal status: ${status}`}
|
||||
/>
|
||||
<button
|
||||
className="terminal-header-button"
|
||||
onClick={() => onFontSizeChange(-1)}
|
||||
type="button"
|
||||
aria-label="Decrease font size"
|
||||
>
|
||||
A-
|
||||
</button>
|
||||
<button
|
||||
className="terminal-header-button"
|
||||
onClick={() => onFontSizeChange(1)}
|
||||
type="button"
|
||||
aria-label="Increase font size"
|
||||
>
|
||||
A+
|
||||
</button>
|
||||
<button
|
||||
className="terminal-header-button"
|
||||
onClick={onShowResetConfirm}
|
||||
type="button"
|
||||
aria-label="Reset terminal"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
className="terminal-close"
|
||||
onClick={onToggleFullscreen}
|
||||
type="button"
|
||||
title="Exit fullscreen (Esc)"
|
||||
>
|
||||
Exit
|
||||
</button>
|
||||
</div>
|
||||
{showResetConfirm && (
|
||||
<div className="terminal-reset-confirm">
|
||||
<div className="terminal-reset-confirm-content">
|
||||
<p>
|
||||
Reset terminal? This will kill the current shell session and
|
||||
start fresh.
|
||||
</p>
|
||||
<div className="terminal-reset-confirm-buttons">
|
||||
<button
|
||||
className="terminal-reset-confirm-button cancel"
|
||||
onClick={onHideResetConfirm}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="terminal-reset-confirm-button confirm"
|
||||
onClick={() => {
|
||||
onHideResetConfirm();
|
||||
onReset();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<TerminalSessionTabs
|
||||
sessions={sessionInfos}
|
||||
activeSessionId={activeSessionId}
|
||||
onSelect={onSelect}
|
||||
onClose={onClose}
|
||||
onCreate={onCreate}
|
||||
onRename={onRename}
|
||||
isMobile={false}
|
||||
/>
|
||||
)}
|
||||
<div className="terminal-page-content">
|
||||
{error && <div className="terminal-error-banner">{error}</div>}
|
||||
{sessions
|
||||
.filter((session) => session.id === activeSessionId)
|
||||
.map((session) => (
|
||||
<div key={session.id} className="terminal-instance active">
|
||||
<TerminalComponent
|
||||
ref={terminalRefs.current[session.id]}
|
||||
instanceId={instanceId}
|
||||
sessionId={session.id}
|
||||
onClose={() => onClose(session.id)}
|
||||
isMobile={false}
|
||||
showControls={!isFullscreen}
|
||||
onTerminalReady={onTerminalReady}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{sessions.length === 0 && !loading && (
|
||||
<div className="terminal-empty-state">
|
||||
<p>No terminal sessions. Press Alt+Shift+N to create one.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
import React from "react";
|
||||
import { TerminalComponent, type TerminalRef } from "./terminal";
|
||||
import { TerminalSessionTabs, type TerminalSessionInfo } from "./terminal-session-tabs";
|
||||
import { Icon } from "../../icon";
|
||||
import { SpecialKeysStrip } from "./special-keys-strip";
|
||||
import { SpecialKeysPanel } from "./special-keys-panel";
|
||||
import type { ModifierKey } from "../../../hooks/use-special-keys";
|
||||
import type { TerminalSession } from "../../../api/terminal";
|
||||
|
||||
interface Props {
|
||||
instanceId: string;
|
||||
sessions: TerminalSession[];
|
||||
sessionInfos: TerminalSessionInfo[];
|
||||
activeSessionId: string;
|
||||
terminalRefs: React.MutableRefObject<Record<string, React.RefObject<TerminalRef>>>;
|
||||
status: string;
|
||||
error: string | null;
|
||||
loading: boolean;
|
||||
isKeyboardOpen: boolean;
|
||||
keyboardHeight: number;
|
||||
isVisible: boolean;
|
||||
activeModifier: ModifierKey | null;
|
||||
showSpecialKeysPanel: boolean;
|
||||
onToggleHeader: () => void;
|
||||
onNavigateBack: () => void;
|
||||
onFontSizeChange: (delta: number) => void;
|
||||
onSelect: (id: string) => void;
|
||||
onClose: (id: string) => void;
|
||||
onCreate: () => void;
|
||||
onRename: (id: string, name: string) => void;
|
||||
onTerminalReady: (
|
||||
sendData: (data: string) => void,
|
||||
status: "connecting" | "connected" | "disconnected" | "error" | "resetting",
|
||||
focusInput: () => void,
|
||||
changeFontSize: (delta: number) => void,
|
||||
) => void;
|
||||
onSendKey: (data: string) => void;
|
||||
onModifierChange: (mod: ModifierKey | null) => void;
|
||||
onShowSpecialKeys: () => void;
|
||||
onHideSpecialKeys: () => void;
|
||||
onKeepFocus: () => void;
|
||||
}
|
||||
|
||||
export const MobileTerminalView: React.FC<Props> = ({
|
||||
instanceId,
|
||||
sessions,
|
||||
sessionInfos,
|
||||
activeSessionId,
|
||||
terminalRefs,
|
||||
status,
|
||||
error,
|
||||
loading,
|
||||
isKeyboardOpen,
|
||||
keyboardHeight,
|
||||
isVisible,
|
||||
activeModifier,
|
||||
showSpecialKeysPanel,
|
||||
onToggleHeader,
|
||||
onNavigateBack,
|
||||
onFontSizeChange,
|
||||
onSelect,
|
||||
onClose,
|
||||
onCreate,
|
||||
onRename,
|
||||
onTerminalReady,
|
||||
onSendKey,
|
||||
onModifierChange,
|
||||
onShowSpecialKeys,
|
||||
onHideSpecialKeys,
|
||||
onKeepFocus,
|
||||
}) => {
|
||||
const activeSession = sessions.find((s) => s.id === activeSessionId);
|
||||
|
||||
return (
|
||||
<section className="terminal-page mobile">
|
||||
<div className={`mobile-terminal-overlay ${isVisible ? "visible" : "hidden"}`} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="mobile-terminal-toolbar">
|
||||
<div className="mobile-terminal-toolbar-left">
|
||||
<button className="mobile-terminal-toolbtn" onClick={onNavigateBack} 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={() => onFontSizeChange(-1)} type="button" aria-label="Decrease font size">
|
||||
<span style={{ fontSize: "0.75rem" }}>A-</span>
|
||||
</button>
|
||||
<button className="mobile-terminal-toolbtn" onClick={() => onFontSizeChange(1)} type="button" aria-label="Increase font size">
|
||||
<span style={{ fontSize: "1rem" }}>A+</span>
|
||||
</button>
|
||||
<button className="mobile-terminal-toolbtn" onClick={onNavigateBack} 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={onSelect}
|
||||
onClose={onClose}
|
||||
onCreate={onCreate}
|
||||
onRename={onRename}
|
||||
isMobile={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="terminal-page-content mobile-full"
|
||||
style={{ paddingBottom: isKeyboardOpen ? keyboardHeight : 0 }}
|
||||
onClick={onToggleHeader}
|
||||
>
|
||||
{error && <div className="terminal-error-banner">{error}</div>}
|
||||
{sessions
|
||||
.filter((session) => session.id === activeSessionId)
|
||||
.map((session) => (
|
||||
<div key={session.id} className="terminal-instance active">
|
||||
<TerminalComponent
|
||||
ref={terminalRefs.current[session.id]}
|
||||
instanceId={instanceId}
|
||||
sessionId={session.id}
|
||||
onClose={() => onClose(session.id)}
|
||||
isMobile={true}
|
||||
showControls={false}
|
||||
activeModifier={activeModifier}
|
||||
onModifierChange={onModifierChange}
|
||||
onTerminalReady={onTerminalReady}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{sessions.length === 0 && !loading && (
|
||||
<div className="terminal-empty-state">
|
||||
<p>No terminal sessions. Press Alt+Shift+N to create one.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SpecialKeysStrip
|
||||
onSend={onSendKey}
|
||||
isVisible={!showSpecialKeysPanel}
|
||||
onMoreClick={onShowSpecialKeys}
|
||||
onKeepFocus={onKeepFocus}
|
||||
activeModifier={activeModifier}
|
||||
onModifierChange={onModifierChange}
|
||||
/>
|
||||
|
||||
<SpecialKeysPanel
|
||||
onSend={onSendKey}
|
||||
isOpen={showSpecialKeysPanel}
|
||||
onClose={onHideSpecialKeys}
|
||||
onKeepFocus={onKeepFocus}
|
||||
activeModifier={activeModifier}
|
||||
onModifierChange={onModifierChange}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user