From ca0820503fda80a47b3ad211f37a11ef5a3808ab Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 14 Nov 2025 12:03:33 -0800 Subject: [PATCH 1/5] new button --- .../components/WorktreeList/WorktreeList.tsx | 188 +------------ .../components/NewTabButton/NewTabButton.tsx | 163 +++++++++++ .../components/NewTabButton/index.ts | 2 + bun.lock | 3 + packages/ui/package.json | 2 + packages/ui/src/components/dropdown-menu.tsx | 255 ++++++++++++++++++ 6 files changed, 433 insertions(+), 180 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/NewTabButton/NewTabButton.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/NewTabButton/index.ts create mode 100644 packages/ui/src/components/dropdown-menu.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/WorktreeList.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/WorktreeList.tsx index 470cc04938e..357de06c08e 100644 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/WorktreeList.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/WorktreeList.tsx @@ -1,7 +1,6 @@ -import { ChevronDown, Monitor, Plus, SquareTerminal } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; import type { Workspace, Worktree } from "shared/types"; import { WorkspacePortIndicator } from "../WorkspacePortIndicator"; +import { NewTabButton } from "./components/NewTabButton"; import { WorktreeItem } from "./components/WorktreeItem"; interface WorktreeListProps { @@ -29,43 +28,6 @@ export function WorktreeList({ selectedWorktreeId, showWorkspaceHeader = false, }: WorktreeListProps) { - // Hooks must be called before any early returns - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [defaultTabType, setDefaultTabType] = useState<"terminal" | "preview">( - () => { - // Load from localStorage or default to "terminal" - const saved = localStorage.getItem("newTabDefaultType"); - return (saved === "preview" ? "preview" : "terminal") as - | "terminal" - | "preview"; - }, - ); - const dropdownRef = useRef(null); - const buttonRef = useRef(null); - const chevronRef = useRef(null); - - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownRef.current && - buttonRef.current && - chevronRef.current && - !dropdownRef.current.contains(event.target as Node) && - !buttonRef.current.contains(event.target as Node) && - !chevronRef.current.contains(event.target as Node) - ) { - setIsDropdownOpen(false); - } - }; - - if (isDropdownOpen) { - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - } - }, [isDropdownOpen]); if (!currentWorkspace) { return ( @@ -85,84 +47,6 @@ export function WorktreeList({ const hasPortForwarding = currentWorkspace.ports && currentWorkspace.ports.length > 0; - const handleAddTerminal = async (updateDefault = false) => { - if (!currentWorkspace || !selectedWorktreeId) return; - setIsDropdownOpen(false); - - if (updateDefault) { - setDefaultTabType("terminal"); - localStorage.setItem("newTabDefaultType", "terminal"); - } - - try { - const result = await window.ipcRenderer.invoke("tab-create", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - name: "New Terminal", - type: "terminal", - }); - - if (result.success) { - const newTabId = result.tab?.id; - if (newTabId) { - onTabSelect(selectedWorktreeId, newTabId); - } - onReload(); - } - } catch (error) { - console.error("Error creating terminal:", error); - } - }; - - const handleAddPreview = async (updateDefault = false) => { - if (!currentWorkspace || !selectedWorktreeId) return; - setIsDropdownOpen(false); - - if (updateDefault) { - setDefaultTabType("preview"); - localStorage.setItem("newTabDefaultType", "preview"); - } - - try { - const worktree = currentWorkspace.worktrees.find( - (wt) => wt.id === selectedWorktreeId, - ); - const previewTabs = - worktree?.tabs?.filter((tab) => tab.type === "preview") || []; - const previewNumber = previewTabs.length + 1; - - const result = await window.ipcRenderer.invoke("tab-create", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - name: `Preview ${previewNumber}`, - type: "preview", - }); - - if (result.success) { - const newTabId = result.tab?.id; - if (newTabId) { - onTabSelect(selectedWorktreeId, newTabId); - } - onReload(); - } - } catch (error) { - console.error("Error creating preview:", error); - } - }; - - const handleCreateDefault = () => { - if (defaultTabType === "terminal") { - handleAddTerminal(); - } else { - handleAddPreview(); - } - }; - - const handleChevronClick = (e: React.MouseEvent) => { - e.stopPropagation(); - setIsDropdownOpen(!isDropdownOpen); - }; - return ( <> {/* Workspace Header - more minimal */} @@ -190,69 +74,13 @@ export function WorktreeList({ ))} {/* Arc-style New Tab Button - styled like a tab at the bottom */} - {selectedWorktreeId && ( -
-
- - - - {/* Dropdown Menu */} - {isDropdownOpen && ( -
- - -
- )} -
-
+ {selectedWorktreeId && currentWorkspace && ( + )} ); diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/NewTabButton/NewTabButton.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/NewTabButton/NewTabButton.tsx new file mode 100644 index 00000000000..c34858799be --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/NewTabButton/NewTabButton.tsx @@ -0,0 +1,163 @@ +import { ChevronDown, Monitor, Plus, SquareTerminal } from "lucide-react"; +import { useState } from "react"; +import type { Workspace } from "shared/types"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@superset/ui/dropdown-menu"; + +interface NewTabButtonProps { + currentWorkspace: Workspace; + selectedWorktreeId: string; + onTabSelect: (worktreeId: string, tabId: string) => void; + onReload: () => void; +} + +export function NewTabButton({ + currentWorkspace, + selectedWorktreeId, + onTabSelect, + onReload, +}: NewTabButtonProps) { + const [defaultTabType, setDefaultTabType] = useState<"terminal" | "preview">( + () => { + // Load from localStorage or default to "terminal" + const saved = localStorage.getItem("newTabDefaultType"); + return (saved === "preview" ? "preview" : "terminal") as + | "terminal" + | "preview"; + }, + ); + + const handleAddTerminal = async (updateDefault = false) => { + if (!currentWorkspace || !selectedWorktreeId) return; + + if (updateDefault) { + setDefaultTabType("terminal"); + localStorage.setItem("newTabDefaultType", "terminal"); + } + + try { + const result = await window.ipcRenderer.invoke("tab-create", { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + name: "New Terminal", + type: "terminal", + }); + + if (result.success) { + const newTabId = result.tab?.id; + if (newTabId) { + onTabSelect(selectedWorktreeId, newTabId); + } + onReload(); + } + } catch (error) { + console.error("Error creating terminal:", error); + } + }; + + const handleAddPreview = async (updateDefault = false) => { + if (!currentWorkspace || !selectedWorktreeId) return; + + if (updateDefault) { + setDefaultTabType("preview"); + localStorage.setItem("newTabDefaultType", "preview"); + } + + try { + const worktree = currentWorkspace.worktrees.find( + (wt) => wt.id === selectedWorktreeId, + ); + const previewTabs = + worktree?.tabs?.filter((tab) => tab.type === "preview") || []; + const previewNumber = previewTabs.length + 1; + + const result = await window.ipcRenderer.invoke("tab-create", { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + name: `Preview ${previewNumber}`, + type: "preview", + }); + + if (result.success) { + const newTabId = result.tab?.id; + if (newTabId) { + onTabSelect(selectedWorktreeId, newTabId); + } + onReload(); + } + } catch (error) { + console.error("Error creating preview:", error); + } + }; + + const handleCreateDefault = () => { + if (defaultTabType === "terminal") { + handleAddTerminal(); + } else { + handleAddPreview(); + } + }; + + return ( +
+
+ + + + + + + + handleAddTerminal(defaultTabType !== "terminal") + } + className={`cursor-pointer ${ + defaultTabType === "terminal" + ? "bg-neutral-700/50 text-neutral-200" + : "text-neutral-300" + }`} + > + + New Terminal + + handleAddPreview(defaultTabType !== "preview")} + className={`cursor-pointer ${ + defaultTabType === "preview" + ? "bg-neutral-700/50 text-neutral-200" + : "text-neutral-300" + }`} + > + + New Preview + + + +
+
+ ); +} + diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/NewTabButton/index.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/NewTabButton/index.ts new file mode 100644 index 00000000000..fb7efae0fc0 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/NewTabButton/index.ts @@ -0,0 +1,2 @@ +export { NewTabButton } from "./NewTabButton"; + diff --git a/bun.lock b/bun.lock index 699c03c4a22..0078f7c349d 100644 --- a/bun.lock +++ b/bun.lock @@ -247,6 +247,7 @@ "dependencies": { "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-scroll-area": "^1.2.10", @@ -738,6 +739,8 @@ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], diff --git a/packages/ui/package.json b/packages/ui/package.json index e61ce74eb48..eb01e898a89 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -14,6 +14,7 @@ "./hover-card": "./src/components/hover-card.tsx", "./dialog": "./src/components/dialog.tsx", "./context-menu": "./src/components/context-menu.tsx", + "./dropdown-menu": "./src/components/dropdown-menu.tsx", "./scroll-area": "./src/components/scroll-area.tsx", "./resizable": "./src/components/resizable.tsx", "./label": "./src/components/label.tsx", @@ -29,6 +30,7 @@ "dependencies": { "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-scroll-area": "^1.2.10", diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx new file mode 100644 index 00000000000..bb0cba5015d --- /dev/null +++ b/packages/ui/src/components/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "../lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} From b983cef4c9f9302c4de2905f8fbf4c9f6c36097e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 14 Nov 2025 12:10:49 -0800 Subject: [PATCH 2/5] fix new modal button --- .../src/renderer/screens/main/MainScreen.tsx | 2 +- .../components/NewTabButton/NewTabButton.tsx | 23 ++--- packages/ui/src/components/dialog.tsx | 3 +- packages/ui/src/components/dropdown-menu.tsx | 89 ++++++++++--------- 4 files changed, 56 insertions(+), 61 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/MainScreen.tsx b/apps/desktop/src/renderer/screens/main/MainScreen.tsx index 7cb26d4b699..b064e2d4286 100644 --- a/apps/desktop/src/renderer/screens/main/MainScreen.tsx +++ b/apps/desktop/src/renderer/screens/main/MainScreen.tsx @@ -118,7 +118,7 @@ export function MainScreen() { {!isSidebarOpen && ( - @@ -473,222 +629,259 @@ export const AddTaskModal: React.FC = ({ onSubmit={handleCreateTask} className="flex-1 flex flex-col min-h-0 overflow-hidden" > - {/* Scrollable content area */} - -
- {/* Title section */} -
-
- - setNewTaskName(e.target.value)} - autoFocus - required - disabled={isCreating} - /> -
-
- -