feat: Make tabs auto expand and contract and scroll
This commit is contained in:
@@ -720,9 +720,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
setVisible(true);
|
setVisible(true);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log(
|
|
||||||
`🔴 Terminal UNMOUNTING - this should NOT happen during drag!`,
|
|
||||||
);
|
|
||||||
isUnmountingRef.current = true;
|
isUnmountingRef.current = true;
|
||||||
shouldNotReconnectRef.current = true;
|
shouldNotReconnectRef.current = true;
|
||||||
isReconnectingRef.current = false;
|
isReconnectingRef.current = false;
|
||||||
@@ -745,21 +742,10 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
}, [xtermRef, terminal]);
|
}, [xtermRef, terminal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(`📡 Terminal connection useEffect triggered:`, {
|
|
||||||
terminal: !!terminal,
|
|
||||||
hostConfig: !!hostConfig,
|
|
||||||
visible,
|
|
||||||
isConnected,
|
|
||||||
isConnecting,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!terminal || !hostConfig || !visible) return;
|
if (!terminal || !hostConfig || !visible) return;
|
||||||
|
|
||||||
if (isConnected || isConnecting) return;
|
if (isConnected || isConnecting) return;
|
||||||
|
|
||||||
console.log(
|
|
||||||
`🔌 Initiating NEW connection - this should only happen on mount!`,
|
|
||||||
);
|
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
|
|
||||||
const readyFonts =
|
const readyFonts =
|
||||||
|
|||||||
@@ -133,23 +133,10 @@ export function AppView({
|
|||||||
const isJustReorder =
|
const isJustReorder =
|
||||||
!lengthChanged && tabIdsChanged && !currentTabChanged && !splitChanged;
|
!lengthChanged && tabIdsChanged && !currentTabChanged && !splitChanged;
|
||||||
|
|
||||||
console.log("AppView useEffect:", {
|
|
||||||
lengthChanged,
|
|
||||||
currentTabChanged,
|
|
||||||
splitChanged,
|
|
||||||
tabIdsChanged,
|
|
||||||
isJustReorder,
|
|
||||||
willCallHideThenFit:
|
|
||||||
(lengthChanged || currentTabChanged || splitChanged) && !isJustReorder,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(lengthChanged || currentTabChanged || splitChanged) &&
|
(lengthChanged || currentTabChanged || splitChanged) &&
|
||||||
!isJustReorder
|
!isJustReorder
|
||||||
) {
|
) {
|
||||||
console.log(
|
|
||||||
"CALLING hideThenFit - this will set ready=false and cause Terminal isVisible to become false!",
|
|
||||||
);
|
|
||||||
hideThenFit();
|
hideThenFit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface TabProps {
|
|||||||
tabType: string;
|
tabType: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
isSplit?: boolean;
|
||||||
onActivate?: () => void;
|
onActivate?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onSplit?: () => void;
|
onSplit?: () => void;
|
||||||
@@ -32,6 +33,7 @@ export function Tab({
|
|||||||
tabType,
|
tabType,
|
||||||
title,
|
title,
|
||||||
isActive,
|
isActive,
|
||||||
|
isSplit = false,
|
||||||
onActivate,
|
onActivate,
|
||||||
onClose,
|
onClose,
|
||||||
onSplit,
|
onSplit,
|
||||||
@@ -47,7 +49,7 @@ export function Tab({
|
|||||||
|
|
||||||
// Firefox-style tab classes using cn utility
|
// Firefox-style tab classes using cn utility
|
||||||
const tabBaseClasses = cn(
|
const tabBaseClasses = cn(
|
||||||
"relative flex items-center gap-1.5 px-3 min-w-fit max-w-[200px]",
|
"relative flex items-center gap-1.5 px-3 w-full min-w-0",
|
||||||
"rounded-t-lg border-t-2 border-l-2 border-r-2",
|
"rounded-t-lg border-t-2 border-l-2 border-r-2",
|
||||||
"transition-all duration-150 h-[42px]",
|
"transition-all duration-150 h-[42px]",
|
||||||
isDragOver &&
|
isDragOver &&
|
||||||
@@ -63,10 +65,34 @@ export function Tab({
|
|||||||
"bg-background/80 text-muted-foreground border-border hover:bg-background/90",
|
"bg-background/80 text-muted-foreground border-border hover:bg-background/90",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Helper function to split title into base and suffix
|
||||||
|
const splitTitle = (fullTitle: string): { base: string; suffix: string } => {
|
||||||
|
const match = fullTitle.match(/^(.*?)(\s*\(\d+\))$/);
|
||||||
|
if (match) {
|
||||||
|
return { base: match[1], suffix: match[2] };
|
||||||
|
}
|
||||||
|
return { base: fullTitle, suffix: "" };
|
||||||
|
};
|
||||||
|
|
||||||
if (tabType === "home") {
|
if (tabType === "home") {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={tabBaseClasses}
|
className={cn(
|
||||||
|
"relative flex items-center gap-1.5 px-3 flex-shrink-0 cursor-pointer",
|
||||||
|
"rounded-t-lg border-t-2 border-l-2 border-r-2",
|
||||||
|
"transition-all duration-150 h-[42px]",
|
||||||
|
isDragOver &&
|
||||||
|
"bg-background/40 text-muted-foreground border-border opacity-60",
|
||||||
|
isDragging && "opacity-70",
|
||||||
|
!isDragOver &&
|
||||||
|
!isDragging &&
|
||||||
|
isActive &&
|
||||||
|
"bg-background text-foreground border-border z-10",
|
||||||
|
!isDragOver &&
|
||||||
|
!isDragging &&
|
||||||
|
!isActive &&
|
||||||
|
"bg-background/80 text-muted-foreground border-border hover:bg-background/90",
|
||||||
|
)}
|
||||||
onClick={!disableActivate ? onActivate : undefined}
|
onClick={!disableActivate ? onActivate : undefined}
|
||||||
style={{
|
style={{
|
||||||
marginBottom: "-2px",
|
marginBottom: "-2px",
|
||||||
@@ -98,18 +124,18 @@ export function Tab({
|
|||||||
? t("nav.userProfile")
|
? t("nav.userProfile")
|
||||||
: t("nav.terminal"));
|
: t("nav.terminal"));
|
||||||
|
|
||||||
|
const { base, suffix } = splitTitle(displayTitle);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={tabBaseClasses}
|
className={cn(tabBaseClasses, "cursor-pointer")}
|
||||||
|
onClick={!disableActivate ? onActivate : undefined}
|
||||||
style={{
|
style={{
|
||||||
marginBottom: "-2px",
|
marginBottom: "-2px",
|
||||||
borderBottom: isActive ? "2px solid white" : "none",
|
borderBottom: isActive ? "2px solid white" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
||||||
className="flex items-center gap-1.5 flex-1 min-w-0"
|
|
||||||
onClick={!disableActivate ? onActivate : undefined}
|
|
||||||
>
|
|
||||||
{isServer ? (
|
{isServer ? (
|
||||||
<ServerIcon className="h-4 w-4 flex-shrink-0" />
|
<ServerIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
) : isFileManager ? (
|
) : isFileManager ? (
|
||||||
@@ -119,7 +145,8 @@ export function Tab({
|
|||||||
) : (
|
) : (
|
||||||
<TerminalIcon className="h-4 w-4 flex-shrink-0" />
|
<TerminalIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
<span className="truncate text-sm">{displayTitle}</span>
|
<span className="truncate text-sm flex-1 min-w-0">{base}</span>
|
||||||
|
{suffix && <span className="text-sm flex-shrink-0">{suffix}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canSplit && (
|
{canSplit && (
|
||||||
@@ -136,7 +163,9 @@ export function Tab({
|
|||||||
disableSplit ? t("nav.cannotSplitTab") : t("nav.splitScreen")
|
disableSplit ? t("nav.cannotSplitTab") : t("nav.splitScreen")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SeparatorVertical className="h-4 w-4" />
|
<SeparatorVertical
|
||||||
|
className={cn("h-4 w-4", isSplit && "text-white")}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -159,21 +188,21 @@ export function Tab({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tabType === "ssh_manager") {
|
if (tabType === "ssh_manager") {
|
||||||
|
const displayTitle = title || t("nav.sshManager");
|
||||||
|
const { base, suffix } = splitTitle(displayTitle);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={tabBaseClasses}
|
className={cn(tabBaseClasses, "cursor-pointer")}
|
||||||
|
onClick={!disableActivate ? onActivate : undefined}
|
||||||
style={{
|
style={{
|
||||||
marginBottom: "-2px",
|
marginBottom: "-2px",
|
||||||
borderBottom: isActive ? "2px solid white" : "none",
|
borderBottom: isActive ? "2px solid white" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
||||||
className="flex items-center gap-1.5 flex-1 min-w-0"
|
<span className="truncate text-sm flex-1 min-w-0">{base}</span>
|
||||||
onClick={!disableActivate ? onActivate : undefined}
|
{suffix && <span className="text-sm flex-shrink-0">{suffix}</span>}
|
||||||
>
|
|
||||||
<span className="truncate text-sm">
|
|
||||||
{title || t("nav.sshManager")}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canClose && (
|
{canClose && (
|
||||||
@@ -195,19 +224,21 @@ export function Tab({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tabType === "admin") {
|
if (tabType === "admin") {
|
||||||
|
const displayTitle = title || t("nav.admin");
|
||||||
|
const { base, suffix } = splitTitle(displayTitle);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={tabBaseClasses}
|
className={cn(tabBaseClasses, "cursor-pointer")}
|
||||||
|
onClick={!disableActivate ? onActivate : undefined}
|
||||||
style={{
|
style={{
|
||||||
marginBottom: "-2px",
|
marginBottom: "-2px",
|
||||||
borderBottom: isActive ? "2px solid white" : "none",
|
borderBottom: isActive ? "2px solid white" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
||||||
className="flex items-center gap-1.5 flex-1 min-w-0"
|
<span className="truncate text-sm flex-1 min-w-0">{base}</span>
|
||||||
onClick={!disableActivate ? onActivate : undefined}
|
{suffix && <span className="text-sm flex-shrink-0">{suffix}</span>}
|
||||||
>
|
|
||||||
<span className="truncate text-sm">{title || t("nav.admin")}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canClose && (
|
{canClose && (
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ export function TopNavbar({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="h-full p-1 pr-2 border-r-2 border-dark-border w-[calc(100%-6rem)] flex items-center overflow-x-auto overflow-y-hidden gap-1 thin-scrollbar"
|
className="h-full p-1 pr-2 border-r-2 border-dark-border w-[calc(100%-6rem)] flex items-center overflow-x-auto gap-1"
|
||||||
>
|
>
|
||||||
{tabs.map((tab: TabData, index: number) => {
|
{tabs.map((tab: TabData, index: number) => {
|
||||||
const isActive = tab.id === currentTab;
|
const isActive = tab.id === currentTab;
|
||||||
@@ -601,12 +601,16 @@ export function TopNavbar({
|
|||||||
cursor: isDraggingThisTab ? "grabbing" : "grab",
|
cursor: isDraggingThisTab ? "grabbing" : "grab",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
WebkitUserSelect: "none",
|
WebkitUserSelect: "none",
|
||||||
|
flex: tab.type === "home" ? "0 0 auto" : "1 1 150px",
|
||||||
|
minWidth: tab.type === "home" ? "auto" : "150px",
|
||||||
|
display: "flex",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab
|
<Tab
|
||||||
tabType={tab.type}
|
tabType={tab.type}
|
||||||
title={tab.title}
|
title={tab.title}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
isSplit={isSplit}
|
||||||
onActivate={() => handleTabActivate(tab.id)}
|
onActivate={() => handleTabActivate(tab.id)}
|
||||||
onClose={
|
onClose={
|
||||||
isTerminal ||
|
isTerminal ||
|
||||||
|
|||||||
Reference in New Issue
Block a user