diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index e813f9f10b08..a2032e703813 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -10,6 +10,7 @@ import { startTransition, Switch, untrack, + type JSX, } from "solid-js" import { createStore } from "solid-js/store" import { useLocation, useMatch, useNavigate, useParams } from "@solidjs/router" @@ -35,11 +36,22 @@ import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/ import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state" import { makeEventListener } from "@solid-primitives/event-listener" import { createResizeObserver } from "@solid-primitives/resize-observer" +import { + DragDropProvider, + DragDropSensors, + DragOverlay, + SortableProvider, + closestCenter, + createSortable, +} from "@thisbeyond/solid-dnd" +import type { DragEvent } from "@thisbeyond/solid-dnd" import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "@/components/titlebar-session-events" import { useGlobal } from "@/context/global" import { decode64 } from "@/utils/base64" import { ServerConnection, useServer } from "@/context/server" -import { tabHref, useTabs, type Tab } from "@/context/tabs" +import { tabHref, tabKey, useTabs, type Tab } from "@/context/tabs" +import { getDraggableId } from "@/utils/solid-dnd" +import { getTabReorderIndex } from "@/pages/session/helpers" import "./titlebar.css" type TauriDesktopWindow = { @@ -409,12 +421,31 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { }) const [tabsAreOverflowing, setTabsAreOverflowing] = createSignal(false) + const [tabDrag, setTabDrag] = createStore({ activeDraggable: undefined as string | undefined }) let tabScrollRef!: HTMLDivElement function refreshTabsAreOverflowing() { setTabsAreOverflowing(tabScrollRef.scrollWidth > tabScrollRef.clientWidth) } + const tabIds = createMemo(() => tabsStore.map(tabKey)) + const handleTabDragStart = (event: unknown) => setTabDrag("activeDraggable", getDraggableId(event)) + const handleTabDragEnd = () => setTabDrag("activeDraggable", undefined) + const handleTabDragOver = (event: DragEvent) => { + const { draggable, droppable } = event + if (!draggable || !droppable) return + const next = [...tabIds()] + const toIndex = getTabReorderIndex(next, draggable.id.toString(), droppable.id.toString()) + if (toIndex === undefined) return + next.splice(toIndex, 0, ...next.splice(next.indexOf(draggable.id.toString()), 1)) + tabsStoreActions.reorder(next) + } + const draggedTab = createMemo(() => { + const key = tabDrag.activeDraggable + if (!key) return + return tabsStore.find((tab) => tabKey(tab) === key) + }) + return (
-
-
{ - tabScrollRef = el - createResizeObserver(el, refreshTabsAreOverflowing) - }} - > + + +
createResizeObserver(el, refreshTabsAreOverflowing)} + data-slot="titlebar-tabs-scroll" + class="flex min-w-0 flex-row items-center gap-1.5 overflow-x-auto no-scrollbar [app-region:no-drag]" + ref={(el) => { + tabScrollRef = el + createResizeObserver(el, refreshTabsAreOverflowing) + }} > - - {(tab, i) => { - let ref!: HTMLDivElement +
createResizeObserver(el, refreshTabsAreOverflowing)} + > + + + {(tab, i) => { + let ref!: HTMLDivElement + const key = tabKey(tab) + + const divider = () => + i() !== 0 && ( +
+ ) + + if (tab.type === "draft") { + return ( + + {divider()} + { + navigateTab(tab) + ref.scrollIntoView({ behavior: "instant" }) + }} + onClose={() => { + const index = tabsStore.findIndex((item) => tabKey(item) === key) + if (index !== -1) tabsStoreActions.removeTab(index) + }} + /> + + ) + } + + return ( + + {divider()} + { + navigateTab(tab) + + ref.scrollIntoView({ behavior: "instant" }) + }} + onClose={() => { + const index = tabsStore.findIndex((item) => tabKey(item) === key) + if (index !== -1) tabsStoreActions.removeTab(index) + }} + active={currentTab() === tab} + activeServer={tab.server === server.key} + forceTruncate={tabsAreOverflowing()} + /> + + ) + }} + + + + {(_) => { + let ref!: HTMLDivElement - const divider = () => - i() !== 0 && ( -
- ) + onMount(() => { + ref.scrollIntoView({ behavior: "instant" }) + }) - if (tab.type === "draft") { return ( <> - {divider()} - + { - navigateTab(tab) - ref.scrollIntoView({ behavior: "instant" }) + onClose={() => { + const tab = tabsStore.at(-1) + if (tab) navigateTab(tab) + else navigate("/") }} - onClose={() => tabsStoreActions.removeTab(i())} /> ) - } - - return ( - <> - {divider()} - { - navigateTab(tab) - - ref.scrollIntoView({ behavior: "instant" }) - }} - onClose={() => tabsStoreActions.removeTab(i())} - active={currentTab() === tab} - activeServer={tab.server === server.key} - forceTruncate={tabsAreOverflowing()} - /> - - ) - }} - - - {(_) => { - let ref!: HTMLDivElement - - onMount(() => { - ref.scrollIntoView({ behavior: "instant" }) - }) - - return ( - <> -
- { - const tab = tabsStore.at(-1) - if (tab) navigateTab(tab) - else navigate("/") - }} - /> - - ) - }} - + }} + +
+