Refactor Settings, FileTree, GitPanel, Shell, and CodeEditor components (#402)

This commit is contained in:
Haileyesus
2026-02-25 19:07:07 +03:00
committed by GitHub
parent 23801e9cc1
commit 5e3a7b69d7
149 changed files with 11627 additions and 8453 deletions

View File

@@ -0,0 +1,59 @@
type ShellConnectionOverlayProps = {
mode: 'loading' | 'connect' | 'connecting';
description: string;
loadingLabel: string;
connectLabel: string;
connectTitle: string;
connectingLabel: string;
onConnect: () => void;
};
export default function ShellConnectionOverlay({
mode,
description,
loadingLabel,
connectLabel,
connectTitle,
connectingLabel,
onConnect,
}: ShellConnectionOverlayProps) {
if (mode === 'loading') {
return (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90">
<div className="text-white">{loadingLabel}</div>
</div>
);
}
if (mode === 'connect') {
return (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90 p-4">
<div className="text-center max-w-sm w-full">
<button
onClick={onConnect}
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center justify-center space-x-2 text-base font-medium w-full sm:w-auto"
title={connectTitle}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span>{connectLabel}</span>
</button>
<p className="text-gray-400 text-sm mt-3 px-2">{description}</p>
</div>
</div>
);
}
return (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90 p-4">
<div className="text-center max-w-sm w-full">
<div className="flex items-center justify-center space-x-3 text-yellow-400">
<div className="w-6 h-6 animate-spin rounded-full border-2 border-yellow-400 border-t-transparent"></div>
<span className="text-base font-medium">{connectingLabel}</span>
</div>
<p className="text-gray-400 text-sm mt-3 px-2">{description}</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,25 @@
type ShellEmptyStateProps = {
title: string;
description: string;
};
export default function ShellEmptyState({ title, description }: ShellEmptyStateProps) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center text-gray-500 dark:text-gray-400">
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
</div>
<h3 className="text-lg font-semibold mb-2">{title}</h3>
<p>{description}</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,87 @@
type ShellHeaderProps = {
isConnected: boolean;
isInitialized: boolean;
isRestarting: boolean;
hasSession: boolean;
sessionDisplayNameShort: string | null;
onDisconnect: () => void;
onRestart: () => void;
statusNewSessionText: string;
statusInitializingText: string;
statusRestartingText: string;
disconnectLabel: string;
disconnectTitle: string;
restartLabel: string;
restartTitle: string;
disableRestart: boolean;
};
export default function ShellHeader({
isConnected,
isInitialized,
isRestarting,
hasSession,
sessionDisplayNameShort,
onDisconnect,
onRestart,
statusNewSessionText,
statusInitializingText,
statusRestartingText,
disconnectLabel,
disconnectTitle,
restartLabel,
restartTitle,
disableRestart,
}: ShellHeaderProps) {
return (
<div className="flex-shrink-0 bg-gray-800 border-b border-gray-700 px-4 py-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
{hasSession && sessionDisplayNameShort && (
<span className="text-xs text-blue-300">({sessionDisplayNameShort}...)</span>
)}
{!hasSession && <span className="text-xs text-gray-400">{statusNewSessionText}</span>}
{!isInitialized && <span className="text-xs text-yellow-400">{statusInitializingText}</span>}
{isRestarting && <span className="text-xs text-blue-400">{statusRestartingText}</span>}
</div>
<div className="flex items-center space-x-3">
{isConnected && (
<button
onClick={onDisconnect}
className="px-3 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700 flex items-center space-x-1"
title={disconnectTitle}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
<span>{disconnectLabel}</span>
</button>
)}
<button
onClick={onRestart}
disabled={disableRestart}
className="text-xs text-gray-400 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
title={restartTitle}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
<span>{restartLabel}</span>
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,113 @@
import { useEffect, useMemo, useState } from 'react';
import type { RefObject } from 'react';
import type { AuthCopyStatus } from '../../types/types';
import { resolveAuthUrlForDisplay } from '../../utils/auth';
type ShellMinimalViewProps = {
terminalContainerRef: RefObject<HTMLDivElement>;
authUrl: string;
authUrlVersion: number;
initialCommand: string | null | undefined;
isConnected: boolean;
openAuthUrlInBrowser: (url: string) => boolean;
copyAuthUrlToClipboard: (url: string) => Promise<boolean>;
};
export default function ShellMinimalView({
terminalContainerRef,
authUrl,
authUrlVersion,
initialCommand,
isConnected,
openAuthUrlInBrowser,
copyAuthUrlToClipboard,
}: ShellMinimalViewProps) {
const [authUrlCopyStatus, setAuthUrlCopyStatus] = useState<AuthCopyStatus>('idle');
const [isAuthPanelHidden, setIsAuthPanelHidden] = useState(false);
const displayAuthUrl = useMemo(
() => resolveAuthUrlForDisplay(initialCommand, authUrl),
[authUrl, initialCommand],
);
// Keep auth panel UI state local to minimal mode and reset it when connection/url changes.
useEffect(() => {
setAuthUrlCopyStatus('idle');
setIsAuthPanelHidden(false);
}, [authUrlVersion, displayAuthUrl, isConnected]);
const hasAuthUrl = Boolean(displayAuthUrl);
const showMobileAuthPanel = hasAuthUrl && !isAuthPanelHidden;
const showMobileAuthPanelToggle = hasAuthUrl && isAuthPanelHidden;
return (
<div className="h-full w-full bg-gray-900 relative">
<div
ref={terminalContainerRef}
className="h-full w-full focus:outline-none"
style={{ outline: 'none' }}
/>
{showMobileAuthPanel && (
<div className="absolute inset-x-0 bottom-14 z-20 border-t border-gray-700/80 bg-gray-900/95 p-3 backdrop-blur-sm md:hidden">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
<p className="text-xs text-gray-300">Open or copy the login URL:</p>
<button
type="button"
onClick={() => setIsAuthPanelHidden(true)}
className="rounded bg-gray-700 px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-gray-100 hover:bg-gray-600"
>
Hide
</button>
</div>
<input
type="text"
value={displayAuthUrl}
readOnly
onClick={(event) => event.currentTarget.select()}
className="w-full rounded border border-gray-600 bg-gray-800 px-2 py-1 text-xs text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label="Authentication URL"
/>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
openAuthUrlInBrowser(displayAuthUrl);
}}
className="flex-1 rounded bg-blue-600 px-3 py-2 text-xs font-medium text-white hover:bg-blue-700"
>
Open URL
</button>
<button
type="button"
onClick={async () => {
const copied = await copyAuthUrlToClipboard(displayAuthUrl);
setAuthUrlCopyStatus(copied ? 'copied' : 'failed');
}}
className="flex-1 rounded bg-gray-700 px-3 py-2 text-xs font-medium text-white hover:bg-gray-600"
>
{authUrlCopyStatus === 'copied' ? 'Copied' : 'Copy URL'}
</button>
</div>
</div>
</div>
)}
{showMobileAuthPanelToggle && (
<div className="absolute bottom-14 right-3 z-20 md:hidden">
<button
type="button"
onClick={() => setIsAuthPanelHidden(false)}
className="rounded bg-gray-800/95 px-3 py-2 text-xs font-medium text-gray-100 shadow-lg backdrop-blur-sm hover:bg-gray-700"
>
Show login URL
</button>
</div>
)}
</div>
);
}