chore: releaes prep
@@ -292,6 +292,10 @@ networks:
|
||||
<td><img src="./repo-images/Image 11.png" alt="Termix Screenshot 11" width="400" /></td>
|
||||
<td><img src="./repo-images/Image 12.png" alt="Termix Screenshot 12" width="400" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="./repo-images/Image 13.png" alt="Termix Screenshot 13" width="400" /></td>
|
||||
<td><img src="./repo-images/Image 14.png" alt="Termix Screenshot 14" width="400" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<sub>Some videos and images may be out of date or may not perfectly showcase features.</sub>
|
||||
|
||||
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 409 KiB After Width: | Height: | Size: 794 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 567 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 404 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 449 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 318 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 113 KiB |
@@ -4,7 +4,14 @@ import { Separator } from "@/components/separator";
|
||||
import { Button } from "@/components/button";
|
||||
import { Sheet, SheetContent } from "@/components/sheet";
|
||||
import { ChevronLeft, ChevronRight, Maximize2 } from "lucide-react";
|
||||
import { useState, useRef, useCallback, useEffect, createRef } from "react";
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
createRef,
|
||||
createPortal,
|
||||
} from "react";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { MobileBottomBar } from "@/shell/MobileBottomBar";
|
||||
import { CommandPalette } from "@/shell/CommandPalette";
|
||||
@@ -166,6 +173,21 @@ export function AppShell({
|
||||
const terminalRefs = useRef<Map<string, ReturnType<typeof createRef>>>(
|
||||
new Map(),
|
||||
);
|
||||
const [paneContentEls, setPaneContentEls] = useState<
|
||||
(HTMLDivElement | null)[]
|
||||
>(Array(6).fill(null));
|
||||
|
||||
const onPaneContentRef = useCallback(
|
||||
(paneIndex: number, el: HTMLDivElement | null) => {
|
||||
setPaneContentEls((prev) => {
|
||||
if (prev[paneIndex] === el) return prev;
|
||||
const next = [...prev];
|
||||
next[paneIndex] = el;
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const sidebarTitle: Record<RailView, string> = {
|
||||
hosts: "Hosts",
|
||||
@@ -721,6 +743,7 @@ export function AppShell({
|
||||
onOpenSingletonTab={openSingletonTab}
|
||||
onOpenTab={openTab}
|
||||
onTerminalResize={resizeAllTerminals}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -730,11 +753,34 @@ export function AppShell({
|
||||
className="absolute inset-0 flex flex-col"
|
||||
style={{ display: isSplit && !isMobile ? "none" : "flex" }}
|
||||
>
|
||||
{/* Terminal tabs: always in DOM, visibility-toggled so xterm keeps its dimensions */}
|
||||
{/* Terminal tabs: always in DOM, visibility-toggled so xterm keeps its dimensions.
|
||||
When split is active and this tab is assigned to a pane, portal it there. */}
|
||||
{tabs
|
||||
.filter((tab) => tab.type === "terminal")
|
||||
.map((tab) => {
|
||||
const visible = tab.id === activeTabId;
|
||||
const paneIdx = isSplit ? paneTabIds.indexOf(tab.id) : -1;
|
||||
const inPane = paneIdx !== -1;
|
||||
const paneEl = inPane ? paneContentEls[paneIdx] : null;
|
||||
const visible = inPane || tab.id === activeTabId;
|
||||
|
||||
const terminalContent = renderTabContent(
|
||||
tab,
|
||||
openSingletonTab,
|
||||
openTab,
|
||||
closeTab,
|
||||
visible,
|
||||
);
|
||||
|
||||
if (inPane && paneEl) {
|
||||
return createPortal(
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
{terminalContent}
|
||||
</div>,
|
||||
paneEl,
|
||||
tab.id,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tab.id}
|
||||
@@ -742,16 +788,10 @@ export function AppShell({
|
||||
style={{
|
||||
visibility: visible ? "visible" : "hidden",
|
||||
pointerEvents: visible ? "auto" : "none",
|
||||
zIndex: visible ? 1 : 0,
|
||||
zIndex: tab.id === activeTabId && !isSplit ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
{renderTabContent(
|
||||
tab,
|
||||
openSingletonTab,
|
||||
openTab,
|
||||
closeTab,
|
||||
visible,
|
||||
)}
|
||||
{terminalContent}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useEffect, memo } from "react";
|
||||
import React, { useState, useRef, useEffect, memo, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { splitDragState, notifyDragEnd } from "@/lib/splitDragging";
|
||||
import { renderTabContent, tabIcon } from "@/shell/tabUtils";
|
||||
@@ -323,24 +323,39 @@ const Pane = memo(function Pane({
|
||||
isDragging,
|
||||
onOpenSingletonTab,
|
||||
onOpenTab,
|
||||
onPaneContentRef,
|
||||
}: {
|
||||
tab: Tab | null;
|
||||
paneIndex: number;
|
||||
isDragging: boolean;
|
||||
onOpenSingletonTab: (type: TabType) => void;
|
||||
onOpenTab: (host: Host, type: TabType) => void;
|
||||
onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void;
|
||||
}) {
|
||||
const isTerminal = tab?.type === "terminal";
|
||||
const contentRef = useCallback(
|
||||
(el: HTMLDivElement | null) => {
|
||||
onPaneContentRef?.(paneIndex, el);
|
||||
},
|
||||
[paneIndex, onPaneContentRef],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col w-full h-full min-w-0 min-h-0 overflow-hidden">
|
||||
<PaneHeader tab={tab} paneIndex={paneIndex} />
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<div className="flex-1 min-h-0 overflow-hidden relative">
|
||||
{tab ? (
|
||||
<PaneContent
|
||||
key={tab.id}
|
||||
tab={tab}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
/>
|
||||
isTerminal ? (
|
||||
// Terminal tabs are portaled in from AppShell to avoid remounting
|
||||
<div ref={contentRef} className="absolute inset-0" />
|
||||
) : (
|
||||
<PaneContent
|
||||
key={tab.id}
|
||||
tab={tab}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<EmptyPane />
|
||||
)}
|
||||
@@ -366,6 +381,7 @@ const Row = memo(function Row({
|
||||
onOpenTab,
|
||||
onColDivider,
|
||||
onColDividerTouch,
|
||||
onPaneContentRef,
|
||||
}: {
|
||||
rowIdx: number;
|
||||
paneIndices: number[];
|
||||
@@ -382,6 +398,7 @@ const Row = memo(function Row({
|
||||
rowIdx: number,
|
||||
colIdx: number,
|
||||
) => void;
|
||||
onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void;
|
||||
}) {
|
||||
const widths = colWidths ?? [];
|
||||
return (
|
||||
@@ -402,6 +419,7 @@ const Row = memo(function Row({
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
</div>
|
||||
{ci < paneIndices.length - 1 && (
|
||||
@@ -426,6 +444,7 @@ export const SplitView = memo(function SplitView({
|
||||
onOpenSingletonTab,
|
||||
onOpenTab,
|
||||
onTerminalResize,
|
||||
onPaneContentRef,
|
||||
}: {
|
||||
tabs: Tab[];
|
||||
paneTabIds: (string | null)[];
|
||||
@@ -433,6 +452,7 @@ export const SplitView = memo(function SplitView({
|
||||
onOpenSingletonTab: (type: TabType) => void;
|
||||
onOpenTab: (host: Host, type: TabType) => void;
|
||||
onTerminalResize?: () => void;
|
||||
onPaneContentRef?: (paneIndex: number, el: HTMLDivElement | null) => void;
|
||||
}) {
|
||||
const {
|
||||
rowSizes,
|
||||
@@ -485,6 +505,7 @@ export const SplitView = memo(function SplitView({
|
||||
onOpenTab={onOpenTab}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -500,6 +521,7 @@ export const SplitView = memo(function SplitView({
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
</div>
|
||||
<ColDivider
|
||||
@@ -517,6 +539,7 @@ export const SplitView = memo(function SplitView({
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
</div>
|
||||
<RowDivider
|
||||
@@ -533,6 +556,7 @@ export const SplitView = memo(function SplitView({
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -555,6 +579,7 @@ export const SplitView = memo(function SplitView({
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
</div>
|
||||
<ColDivider
|
||||
@@ -568,6 +593,7 @@ export const SplitView = memo(function SplitView({
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -582,6 +608,7 @@ export const SplitView = memo(function SplitView({
|
||||
isDragging={isDragging}
|
||||
onOpenSingletonTab={onOpenSingletonTab}
|
||||
onOpenTab={onOpenTab}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -601,6 +628,7 @@ export const SplitView = memo(function SplitView({
|
||||
onOpenTab={onOpenTab}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
<RowDivider
|
||||
onMouseDown={(e) => onRowDivider(e, 0)}
|
||||
@@ -618,6 +646,7 @@ export const SplitView = memo(function SplitView({
|
||||
onOpenTab={onOpenTab}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -636,6 +665,7 @@ export const SplitView = memo(function SplitView({
|
||||
onOpenTab={onOpenTab}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
<RowDivider
|
||||
onMouseDown={(e) => onRowDivider(e, 0)}
|
||||
@@ -653,6 +683,7 @@ export const SplitView = memo(function SplitView({
|
||||
onOpenTab={onOpenTab}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -671,6 +702,7 @@ export const SplitView = memo(function SplitView({
|
||||
onOpenTab={onOpenTab}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
<RowDivider
|
||||
onMouseDown={(e) => onRowDivider(e, 0)}
|
||||
@@ -688,6 +720,7 @@ export const SplitView = memo(function SplitView({
|
||||
onOpenTab={onOpenTab}
|
||||
onColDivider={onColDivider}
|
||||
onColDividerTouch={onColDividerTouch}
|
||||
onPaneContentRef={onPaneContentRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||