mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat(split-screen): redesign split screen with free-form panes and pane picker
This commit is contained in:
1 parent
5f3372f9f6
commit
81e7aa6274
46 files changed
+5027
-2537
No files matched your search
@@ -10,7 +10,7 @@ import {
|
||||
SquareArrowOutUpRight,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import type { SplitMode, TabType, ToolsTab } from "@/types/ui-types";
|
||||
import type { TabType, ToolsTab } from "@/types/ui-types";
|
||||
import { Skeleton } from "@/components/skeleton";
|
||||
import { readRailPreference, setRailPreference } from "./rail-preferences";
|
||||
import { useRailItems, type RailItemDef } from "./rail-items";
|
||||
@@ -35,7 +35,6 @@ type RailItem =
|
||||
view: RailView;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
dot?: boolean;
|
||||
promotable?: boolean;
|
||||
rightDockable?: boolean;
|
||||
useBadge?: () => number | null | undefined;
|
||||
@@ -51,7 +50,6 @@ type RailItem =
|
||||
|
||||
function buildRailButtons(
|
||||
items: RailItemDef[],
|
||||
splitMode: SplitMode,
|
||||
t: (key: string) => string,
|
||||
hidden: Set<string>,
|
||||
): RailItem[] {
|
||||
@@ -71,7 +69,6 @@ function buildRailButtons(
|
||||
view: item.id as RailView,
|
||||
icon: <Icon size={16} />,
|
||||
title: t(item.labelKey),
|
||||
dot: item.id === "split-screen" ? splitMode !== "none" : undefined,
|
||||
promotable: item.promotable,
|
||||
rightDockable: item.rightDockable,
|
||||
useBadge: item.useBadge,
|
||||
@@ -108,7 +105,6 @@ const btnStyle = { margin: `0 ${rem(4)}`, padding: `0 ${rem(8)}` };
|
||||
export function AppRail({
|
||||
railView,
|
||||
sidebarOpen,
|
||||
splitMode,
|
||||
username,
|
||||
isAdmin,
|
||||
pluginsSettled = true,
|
||||
@@ -119,7 +115,6 @@ export function AppRail({
|
||||
}: {
|
||||
railView: RailView;
|
||||
sidebarOpen: boolean;
|
||||
splitMode: SplitMode;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
/** False while plugins are still registering their rail items. */
|
||||
@@ -215,7 +210,6 @@ export function AppRail({
|
||||
const railExpanded = pinned || (expandOnHover && hovered);
|
||||
const railButtons = buildRailButtons(
|
||||
railItems.filter((item) => item.placement !== "footer"),
|
||||
splitMode,
|
||||
t,
|
||||
hiddenTabs,
|
||||
);
|
||||
@@ -363,9 +357,6 @@ export function AppRail({
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
{item.dot && (
|
||||
<span className="absolute top-0.5 right-0.5 size-1.5 rounded-full bg-accent-brand" />
|
||||
)}
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -1,414 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/button";
|
||||
import { Separator } from "@/components/separator";
|
||||
import { LayoutPanelLeft, X, ChevronDown } from "lucide-react";
|
||||
import { PANE_COUNTS, SPLIT_MODES } from "@/lib/theme";
|
||||
import { tabJumpHotkeyKeys } from "@/lib/tab-jump-hotkey";
|
||||
import { tabIcon } from "@/shell/tabUtils";
|
||||
import type { Tab, SplitMode } from "@/types/ui-types";
|
||||
|
||||
const LAYOUT_PREVIEWS: Record<SplitMode, React.ReactNode> = {
|
||||
none: <div className="size-full border-2 border-current" />,
|
||||
"2-way": (
|
||||
<div className="flex gap-0.5 size-full">
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
</div>
|
||||
),
|
||||
"2-way-horizontal": (
|
||||
<div className="flex flex-col gap-0.5 size-full">
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
</div>
|
||||
),
|
||||
"3-way": (
|
||||
<div className="flex gap-0.5 size-full">
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
<div className="flex flex-col flex-1 gap-0.5">
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
"3-way-horizontal": (
|
||||
<div className="flex flex-col gap-0.5 size-full">
|
||||
<div className="flex gap-0.5 flex-1">
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
</div>
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
</div>
|
||||
),
|
||||
"4-way": (
|
||||
<div className="grid grid-cols-2 grid-rows-2 gap-0.5 size-full">
|
||||
<div className="border-2 border-current" />
|
||||
<div className="border-2 border-current" />
|
||||
<div className="border-2 border-current" />
|
||||
<div className="border-2 border-current" />
|
||||
</div>
|
||||
),
|
||||
"5-way": (
|
||||
<div className="flex flex-col gap-0.5 size-full">
|
||||
<div className="flex gap-0.5 flex-1">
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
</div>
|
||||
<div className="flex gap-0.5 flex-1">
|
||||
<div className="flex-1 border-2 border-current" />
|
||||
<div className="flex-[2] border-2 border-current" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
"6-way": (
|
||||
<div className="grid grid-cols-3 grid-rows-2 gap-0.5 size-full">
|
||||
<div className="border-2 border-current" />
|
||||
<div className="border-2 border-current" />
|
||||
<div className="border-2 border-current" />
|
||||
<div className="border-2 border-current" />
|
||||
<div className="border-2 border-current" />
|
||||
<div className="border-2 border-current" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export function SplitScreenPanel({
|
||||
tabs,
|
||||
splitMode,
|
||||
setSplitMode,
|
||||
paneTabIds,
|
||||
setPaneTabIds,
|
||||
onAssignPane,
|
||||
}: {
|
||||
tabs: Tab[];
|
||||
splitMode: SplitMode;
|
||||
setSplitMode: (m: SplitMode) => void;
|
||||
paneTabIds: (string | null)[];
|
||||
setPaneTabIds: (ids: (string | null)[]) => void;
|
||||
onAssignPane: (paneIndex: number, tabId: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const paneCount = PANE_COUNTS[splitMode];
|
||||
const [draggingTabId, setDraggingTabId] = useState<string | null>(null);
|
||||
const [dragOverPane, setDragOverPane] = useState<number | null>(null);
|
||||
const [quickAssignPane, setQuickAssignPane] = useState<number | null>(null);
|
||||
|
||||
function handleDrop(paneIndex: number) {
|
||||
if (draggingTabId === null) return;
|
||||
onAssignPane(paneIndex, draggingTabId);
|
||||
setDraggingTabId(null);
|
||||
setDragOverPane(null);
|
||||
}
|
||||
|
||||
function clearPane(paneIndex: number) {
|
||||
const next = [...paneTabIds];
|
||||
next[paneIndex] = null;
|
||||
setPaneTabIds(next);
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
setSplitMode("none");
|
||||
setPaneTabIds(Array(6).fill(null));
|
||||
}
|
||||
|
||||
const activeCount = paneTabIds
|
||||
.slice(0, Math.max(paneCount, 0))
|
||||
.filter(Boolean).length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("newUi.sidebar.splitScreen.layoutTitle")}
|
||||
</span>
|
||||
{splitMode !== "none" && (
|
||||
<span className="text-xs border border-accent-brand/40 text-accent-brand px-1.5 py-0.5 leading-tight">
|
||||
{splitMode}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{SPLIT_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
onClick={() => setSplitMode(mode.id)}
|
||||
className={`flex flex-col items-center gap-1.5 p-2 border transition-colors ${
|
||||
splitMode === mode.id
|
||||
? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand"
|
||||
: "border-border text-muted-foreground hover:text-foreground hover:border-muted-foreground/50"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-10 h-7 ${splitMode === mode.id ? "text-accent-brand" : "text-muted-foreground/40"}`}
|
||||
>
|
||||
{LAYOUT_PREVIEWS[mode.id]}
|
||||
</div>
|
||||
<span className="text-xs font-semibold">{mode.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{splitMode === "none" ? (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-6 text-center border border-dashed border-border">
|
||||
<LayoutPanelLeft className="size-8 text-muted-foreground/30" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("newUi.sidebar.splitScreen.selectLayoutAbove")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground/60">
|
||||
{t("newUi.sidebar.splitScreen.selectLayoutHint")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("newUi.sidebar.splitScreen.panesTitle")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{activeCount}/{paneCount} assigned
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{Array.from({ length: paneCount }).map((_, i) => {
|
||||
const assignedId = paneTabIds[i];
|
||||
const assignedTab = assignedId
|
||||
? tabs.find((t) => t.id === assignedId)
|
||||
: null;
|
||||
const isOver = dragOverPane === i;
|
||||
const isQuickAssignOpen = quickAssignPane === i;
|
||||
return (
|
||||
<div key={i} className="flex flex-col gap-0.5">
|
||||
<div
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setDragOverPane(i);
|
||||
}}
|
||||
onDragLeave={() => setDragOverPane(null)}
|
||||
onDrop={() => handleDrop(i)}
|
||||
className={`relative flex flex-col items-center justify-center gap-1 p-2 min-h-[52px] border transition-colors ${
|
||||
isOver
|
||||
? "border-accent-brand bg-accent-brand/10"
|
||||
: assignedTab
|
||||
? "border-accent-brand/30 bg-accent-brand/5"
|
||||
: "border-dashed border-border/60 bg-muted/5 hover:border-border hover:bg-muted/10"
|
||||
}`}
|
||||
>
|
||||
<span className="absolute top-1 left-1.5 text-[10px] text-muted-foreground/40 font-mono leading-none">
|
||||
{i + 1}
|
||||
</span>
|
||||
{assignedTab ? (
|
||||
<>
|
||||
<div className="flex items-center gap-1 px-1 w-full justify-center">
|
||||
<span className="text-accent-brand shrink-0">
|
||||
{tabIcon(assignedTab.type)}
|
||||
</span>
|
||||
<span className="text-xs font-semibold truncate max-w-[70px] text-foreground">
|
||||
{assignedTab.type === "dashboard"
|
||||
? t("newUi.sidebar.splitScreen.dashboard")
|
||||
: assignedTab.label}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => clearPane(i)}
|
||||
className="absolute top-0.5 right-0.5 size-4 flex items-center justify-center text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="size-2.5" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground/40">
|
||||
{isOver
|
||||
? t("newUi.sidebar.splitScreen.dropHere")
|
||||
: t("newUi.sidebar.splitScreen.emptyPane")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick assign button */}
|
||||
{!assignedTab && (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() =>
|
||||
setQuickAssignPane(isQuickAssignOpen ? null : i)
|
||||
}
|
||||
className="flex items-center justify-center gap-1 w-full py-0.5 text-[10px] text-muted-foreground/60 hover:text-accent-brand hover:bg-accent-brand/5 border border-dashed border-border/40 hover:border-accent-brand/30 transition-colors"
|
||||
>
|
||||
{t("newUi.sidebar.splitScreen.quickAssign")}
|
||||
<ChevronDown
|
||||
className={`size-2.5 transition-transform ${isQuickAssignOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
{isQuickAssignOpen && (
|
||||
<div className="absolute top-full left-0 right-0 z-20 bg-popover border border-border shadow-md mt-0.5 max-h-40 overflow-y-auto">
|
||||
{tabs
|
||||
.filter(
|
||||
(tab) =>
|
||||
tab.type !== "dashboard" &&
|
||||
!paneTabIds.includes(tab.id),
|
||||
)
|
||||
.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => {
|
||||
onAssignPane(i, tab.id);
|
||||
setQuickAssignPane(null);
|
||||
}}
|
||||
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs text-left hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<span className="text-muted-foreground shrink-0">
|
||||
{tabIcon(tab.type)}
|
||||
</span>
|
||||
<span className="truncate">{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
{tabs.filter(
|
||||
(tab) =>
|
||||
tab.type !== "dashboard" &&
|
||||
!paneTabIds.includes(tab.id),
|
||||
).length === 0 && (
|
||||
<div className="px-2 py-2 text-xs text-muted-foreground/60">
|
||||
All tabs assigned
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("newUi.sidebar.splitScreen.openTabsTitle")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground/60">
|
||||
{t("newUi.sidebar.splitScreen.dragTabsHint")}
|
||||
</span>
|
||||
<div className="flex flex-col gap-1 mt-0.5">
|
||||
{tabs.map((tab) => {
|
||||
const paneIdx = paneTabIds.indexOf(tab.id);
|
||||
const isAssigned = paneIdx !== -1;
|
||||
return (
|
||||
<div
|
||||
key={tab.id}
|
||||
draggable={!isAssigned}
|
||||
onDragStart={() => !isAssigned && setDraggingTabId(tab.id)}
|
||||
onDragEnd={() => {
|
||||
setDraggingTabId(null);
|
||||
setDragOverPane(null);
|
||||
}}
|
||||
className={`flex items-center gap-2 px-2.5 py-2 border select-none transition-colors ${
|
||||
isAssigned
|
||||
? "border-accent-brand/20 bg-accent-brand/5 opacity-60 cursor-default"
|
||||
: draggingTabId === tab.id
|
||||
? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand cursor-grabbing"
|
||||
: "border-border hover:border-muted-foreground/40 hover:bg-muted/30 cursor-grab active:cursor-grabbing"
|
||||
}`}
|
||||
>
|
||||
<span className="text-muted-foreground shrink-0">
|
||||
{tabIcon(tab.type)}
|
||||
</span>
|
||||
<span className="text-xs font-medium flex-1 truncate">
|
||||
{tab.type === "dashboard"
|
||||
? t("newUi.sidebar.splitScreen.dashboard")
|
||||
: tab.label}
|
||||
</span>
|
||||
{isAssigned ? (
|
||||
<span className="text-[10px] text-accent-brand/60 font-mono shrink-0">
|
||||
{t("newUi.sidebar.splitScreen.alreadyAssigned", {
|
||||
index: paneIdx + 1,
|
||||
})}
|
||||
</span>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-px opacity-30 shrink-0">
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
<div className="size-1 bg-muted-foreground rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full text-xs text-muted-foreground"
|
||||
onClick={resetAll}
|
||||
>
|
||||
<X className="size-3" />
|
||||
{t("newUi.sidebar.splitScreen.clearSplitScreen")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("newUi.sidebar.splitScreen.hotkeysTitle")}
|
||||
</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
{[
|
||||
{
|
||||
keys: ["Ctrl", "Shift", "\\"],
|
||||
label: t("newUi.sidebar.splitScreen.hotkeysSplitRight"),
|
||||
},
|
||||
{
|
||||
keys: ["Ctrl", "Shift", "-"],
|
||||
label: t("newUi.sidebar.splitScreen.hotkeysSplitBelow"),
|
||||
},
|
||||
{
|
||||
keys: ["Alt", "↑↓←→"],
|
||||
label: t("newUi.sidebar.splitScreen.hotkeysNavigatePane"),
|
||||
},
|
||||
{
|
||||
keys: ["Ctrl", "Shift", "]"],
|
||||
label: t("newUi.sidebar.splitScreen.hotkeysNextTab"),
|
||||
},
|
||||
{
|
||||
keys: ["Ctrl", "Shift", "["],
|
||||
label: t("newUi.sidebar.splitScreen.hotkeysPrevTab"),
|
||||
},
|
||||
{
|
||||
keys: tabJumpHotkeyKeys(),
|
||||
label: t("newUi.sidebar.splitScreen.hotkeysJumpTab"),
|
||||
},
|
||||
].map(({ keys, label }) => (
|
||||
<div
|
||||
key={label}
|
||||
className="flex items-center justify-between gap-2"
|
||||
>
|
||||
<span className="text-xs text-muted-foreground/70 flex-1 min-w-0 truncate">
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex items-center gap-0.5 shrink-0">
|
||||
{keys.map((k, i) => (
|
||||
<kbd
|
||||
key={i}
|
||||
className="inline-flex items-center px-1 py-0.5 text-[10px] font-mono bg-muted border border-border/60 text-muted-foreground leading-none"
|
||||
>
|
||||
{k}
|
||||
</kbd>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Cloud,
|
||||
Hammer,
|
||||
KeyRound,
|
||||
LayoutPanelLeft,
|
||||
Plug,
|
||||
Server,
|
||||
Settings,
|
||||
@@ -112,12 +111,6 @@ export const RAIL_ITEMS: RailItemDef[] = [
|
||||
promotable: true,
|
||||
rightDockable: true,
|
||||
},
|
||||
{
|
||||
id: "split-screen",
|
||||
icon: LayoutPanelLeft,
|
||||
labelKey: "nav.splitScreen",
|
||||
separatorAfter: true,
|
||||
},
|
||||
{
|
||||
id: "sync",
|
||||
icon: Cloud,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
GripVertical,
|
||||
Key,
|
||||
KeyRound,
|
||||
LayoutPanelLeft,
|
||||
Link,
|
||||
MemoryStick,
|
||||
MoreHorizontal,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
@@ -87,6 +89,12 @@ import {
|
||||
} from "@/sidebar/host-contributions";
|
||||
import { shell } from "@/plugin-host/shell-bridge";
|
||||
import type { TabShellCallbacks } from "@/shell/tab-registry";
|
||||
import {
|
||||
openInSplit,
|
||||
useSplitTargets,
|
||||
type SplitOpenTarget,
|
||||
} from "@/shell/split/split-targets";
|
||||
import { MAX_PANES } from "@/shell/split/split-tree";
|
||||
|
||||
export function statusCheckEnabled(host: Host): boolean {
|
||||
return host.statusCheckEnabled !== false;
|
||||
@@ -276,6 +284,7 @@ export function HostItem({
|
||||
const pluginActions = hostActionsFor(allHostActions, host);
|
||||
const badges = hostBadgesFor(useHostBadges(), host);
|
||||
const pluginMenuItems = hostMenuItemsFor(useHostContextMenuItems(), host);
|
||||
const splitTargets = useSplitTargets();
|
||||
const statusScheme = useStatusColorScheme();
|
||||
const { initialLoadComplete } = useServerStatusMeta();
|
||||
const statusCheckOn = statusCheckEnabled(host);
|
||||
@@ -647,6 +656,88 @@ export function HostItem({
|
||||
{t("hosts.openInNewTab")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{defaultAction &&
|
||||
(splitTargets.canStartSplit || splitTargets.splits.length > 0) && (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<LayoutPanelLeft className="size-3.5 mr-2" />
|
||||
{t("splitScreen.openInSplit")}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="max-w-72">
|
||||
{(() => {
|
||||
const openAt = (target: SplitOpenTarget) =>
|
||||
openInSplit(target, () =>
|
||||
openHostTab(defaultAction, { forceNewTab: true }),
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{splitTargets.canStartSplit && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openAt({ kind: "newSplit" });
|
||||
}}
|
||||
>
|
||||
{t("splitScreen.openInNewSplit")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{splitTargets.splits.map((split) => (
|
||||
<div key={split.id}>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="truncate text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{split.label}
|
||||
</DropdownMenuLabel>
|
||||
{split.panes.map((pane) => (
|
||||
<DropdownMenuItem
|
||||
key={pane.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openAt({
|
||||
kind: "pane",
|
||||
splitTabId: split.id,
|
||||
paneId: pane.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="truncate">
|
||||
{pane.label
|
||||
? t("splitScreen.paneItem", {
|
||||
index: pane.index,
|
||||
label: pane.label,
|
||||
})
|
||||
: t("splitScreen.paneEmptyItem", {
|
||||
index: pane.index,
|
||||
})}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuItem
|
||||
disabled={split.full}
|
||||
title={
|
||||
split.full
|
||||
? t("splitScreen.maxPanes", {
|
||||
count: MAX_PANES,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openAt({
|
||||
kind: "newPane",
|
||||
splitTabId: split.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("splitScreen.newPaneRight")}
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{onEditHost && (
|
||||
<DropdownMenuItem
|
||||
|
||||
Reference in new issue
Block a user