Skip to content

Commit 47e4310

Browse files
committed
✨ Implement task: context menu for tabs
Task ID: cabe8ff5-bedc-4ea4-8769-0ef1a96f94ae Generated by PostHog Agent Plan Summary: I'll analyze the codebase and create a detailed implementation plan for adding a context menu to tabs with "Close other tabs" and "Close tabs to the right" functionality. This commit implements the changes described in the task plan.
1 parent e0f11dc commit 47e4310

File tree

2 files changed

+137
-62
lines changed

2 files changed

+137
-62
lines changed

src/renderer/components/TabBar.tsx

Lines changed: 93 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
import { Cross2Icon } from "@radix-ui/react-icons";
2-
import { Box, Flex, IconButton, Kbd, Text } from "@radix-ui/themes";
2+
import {
3+
Box,
4+
ContextMenu,
5+
Flex,
6+
IconButton,
7+
Kbd,
8+
Text,
9+
} from "@radix-ui/themes";
310
import type React from "react";
411
import { useCallback, useState } from "react";
512
import { useHotkeys } from "react-hotkeys-hook";
613
import { useTabStore } from "../stores/tabStore";
714

815
export function TabBar() {
9-
const { tabs, activeTabId, setActiveTab, closeTab, reorderTabs } =
10-
useTabStore();
16+
const {
17+
tabs,
18+
activeTabId,
19+
setActiveTab,
20+
closeTab,
21+
closeOtherTabs,
22+
closeTabsToRight,
23+
reorderTabs,
24+
} = useTabStore();
1125
const [draggedTab, setDraggedTab] = useState<string | null>(null);
1226
const [dragOverTab, setDragOverTab] = useState<string | null>(null);
1327
const [dropPosition, setDropPosition] = useState<"left" | "right" | null>(
@@ -168,66 +182,83 @@ export function TabBar() {
168182
const showRightIndicator = isDragOver && dropPosition === "right";
169183

170184
return (
171-
<Flex
172-
key={tab.id}
173-
className={`no-drag group relative cursor-pointer border-gray-6 border-r border-b-2 transition-colors ${
174-
tab.id === activeTabId
175-
? "border-b-accent-8 bg-accent-3 text-accent-12"
176-
: "border-b-transparent text-gray-11 hover:bg-gray-3 hover:text-gray-12"
177-
} ${isDragging ? "opacity-50" : ""}`}
178-
align="center"
179-
px="4"
180-
draggable
181-
onClick={() => setActiveTab(tab.id)}
182-
onDragStart={(e) => handleDragStart(e, tab.id)}
183-
onDragOver={(e) => handleDragOver(e, tab.id)}
184-
onDragLeave={handleDragLeave}
185-
onDrop={(e) => handleDrop(e, tab.id)}
186-
onDragEnd={handleDragEnd}
187-
>
188-
{showLeftIndicator && (
189-
<Box
190-
className="absolute top-0 bottom-0 left-0 z-10 w-0.5 bg-accent-8"
191-
style={{ marginLeft: "-1px" }}
192-
/>
193-
)}
194-
195-
{showRightIndicator && (
196-
<Box
197-
className="absolute top-0 right-0 bottom-0 z-10 w-0.5 bg-accent-8"
198-
style={{ marginRight: "-1px" }}
199-
/>
200-
)}
201-
{index < 9 && (
202-
<Kbd size="1" className="mr-2 opacity-70">
203-
{navigator.platform.includes("Mac") ? "⌘" : "Ctrl+"}
204-
{index + 1}
205-
</Kbd>
206-
)}
207-
208-
<Text
209-
size="2"
210-
className="max-w-[200px] select-none overflow-hidden text-ellipsis whitespace-nowrap"
211-
mr="2"
212-
>
213-
{tab.title}
214-
</Text>
215-
216-
{tabs.length > 1 && (
217-
<IconButton
218-
size="1"
219-
variant="ghost"
220-
color={tab.id !== activeTabId ? "gray" : undefined}
221-
className="opacity-0 transition-opacity group-hover:opacity-100"
222-
onClick={(e) => {
223-
e.stopPropagation();
224-
closeTab(tab.id);
225-
}}
185+
<ContextMenu.Root key={tab.id}>
186+
<ContextMenu.Trigger>
187+
<Flex
188+
className={`no-drag group relative cursor-pointer border-gray-6 border-r border-b-2 transition-colors ${
189+
tab.id === activeTabId
190+
? "border-b-accent-8 bg-accent-3 text-accent-12"
191+
: "border-b-transparent text-gray-11 hover:bg-gray-3 hover:text-gray-12"
192+
} ${isDragging ? "opacity-50" : ""}`}
193+
align="center"
194+
px="4"
195+
draggable
196+
onClick={() => setActiveTab(tab.id)}
197+
onDragStart={(e) => handleDragStart(e, tab.id)}
198+
onDragOver={(e) => handleDragOver(e, tab.id)}
199+
onDragLeave={handleDragLeave}
200+
onDrop={(e) => handleDrop(e, tab.id)}
201+
onDragEnd={handleDragEnd}
226202
>
227-
<Cross2Icon />
228-
</IconButton>
229-
)}
230-
</Flex>
203+
{showLeftIndicator && (
204+
<Box
205+
className="absolute top-0 bottom-0 left-0 z-10 w-0.5 bg-accent-8"
206+
style={{ marginLeft: "-1px" }}
207+
/>
208+
)}
209+
210+
{showRightIndicator && (
211+
<Box
212+
className="absolute top-0 right-0 bottom-0 z-10 w-0.5 bg-accent-8"
213+
style={{ marginRight: "-1px" }}
214+
/>
215+
)}
216+
{index < 9 && (
217+
<Kbd size="1" className="mr-2 opacity-70">
218+
{navigator.platform.includes("Mac") ? "⌘" : "Ctrl+"}
219+
{index + 1}
220+
</Kbd>
221+
)}
222+
223+
<Text
224+
size="2"
225+
className="max-w-[200px] select-none overflow-hidden text-ellipsis whitespace-nowrap"
226+
mr="2"
227+
>
228+
{tab.title}
229+
</Text>
230+
231+
{tabs.length > 1 && (
232+
<IconButton
233+
size="1"
234+
variant="ghost"
235+
color={tab.id !== activeTabId ? "gray" : undefined}
236+
className="opacity-0 transition-opacity group-hover:opacity-100"
237+
onClick={(e) => {
238+
e.stopPropagation();
239+
closeTab(tab.id);
240+
}}
241+
>
242+
<Cross2Icon />
243+
</IconButton>
244+
)}
245+
</Flex>
246+
</ContextMenu.Trigger>
247+
<ContextMenu.Content>
248+
<ContextMenu.Item
249+
disabled={tabs.length === 1}
250+
onSelect={() => closeOtherTabs(tab.id)}
251+
>
252+
Close other tabs
253+
</ContextMenu.Item>
254+
<ContextMenu.Item
255+
disabled={index === tabs.length - 1}
256+
onSelect={() => closeTabsToRight(tab.id)}
257+
>
258+
Close tabs to the right
259+
</ContextMenu.Item>
260+
</ContextMenu.Content>
261+
</ContextMenu.Root>
231262
);
232263
})}
233264
</Flex>

src/renderer/stores/tabStore.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface TabStore {
99

1010
createTab: (tab: Omit<TabState, "id">) => void;
1111
closeTab: (tabId: string) => void;
12+
closeOtherTabs: (tabId: string) => void;
13+
closeTabsToRight: (tabId: string) => void;
1214
setActiveTab: (tabId: string) => void;
1315
updateTabTitle: (tabId: string, title: string) => void;
1416
reorderTabs: (fromIndex: number, toIndex: number) => void;
@@ -72,6 +74,48 @@ export const useTabStore = create<TabStore>()(
7274
});
7375
},
7476

77+
closeOtherTabs: (tabId) => {
78+
const state = get();
79+
80+
// Ensure we have more than one tab
81+
if (state.tabs.length === 1) return;
82+
83+
// Keep only the tab with the specified tabId
84+
const tabToKeep = state.tabs.find((tab) => tab.id === tabId);
85+
86+
if (!tabToKeep) return;
87+
88+
set({
89+
tabs: [tabToKeep],
90+
activeTabId: tabId,
91+
});
92+
},
93+
94+
closeTabsToRight: (tabId) => {
95+
const state = get();
96+
const tabIndex = state.tabs.findIndex((tab) => tab.id === tabId);
97+
98+
// If tab not found or it's already the last tab, do nothing
99+
if (tabIndex === -1 || tabIndex === state.tabs.length - 1) return;
100+
101+
// Keep only tabs up to and including the specified tab
102+
const newTabs = state.tabs.slice(0, tabIndex + 1);
103+
let newActiveTabId = state.activeTabId;
104+
105+
// If the active tab was closed, select the rightmost remaining tab
106+
const activeTabStillExists = newTabs.some(
107+
(tab) => tab.id === state.activeTabId,
108+
);
109+
if (!activeTabStillExists) {
110+
newActiveTabId = newTabs[newTabs.length - 1].id;
111+
}
112+
113+
set({
114+
tabs: newTabs,
115+
activeTabId: newActiveTabId,
116+
});
117+
},
118+
75119
setActiveTab: (tabId) => {
76120
set({ activeTabId: tabId });
77121
},

0 commit comments

Comments
 (0)