diff --git a/surfsense_web/components/layout/ui/tabs/TabBar.tsx b/surfsense_web/components/layout/ui/tabs/TabBar.tsx index 432d101e4..e74cd0919 100644 --- a/surfsense_web/components/layout/ui/tabs/TabBar.tsx +++ b/surfsense_web/components/layout/ui/tabs/TabBar.tsx @@ -2,7 +2,7 @@ import { useAtomValue, useSetAtom } from "jotai"; import { Plus, X } from "lucide-react"; -import { useCallback, useEffect, useRef } from "react"; +import { Fragment, useCallback, useEffect, useRef, useState } from "react"; import { activeTabIdAtom, closeTabAtom, @@ -45,6 +45,21 @@ export function TabBar({ const switchTab = useSetAtom(switchTabAtom); const closeTab = useSetAtom(closeTabAtom); const scrollRef = useRef(null); + const [hoveredTabIndex, setHoveredTabIndex] = useState(null); + const activeTabIndex = tabs.findIndex((tab) => tab.id === activeTabId); + + const shouldHideSeparator = useCallback( + (separatorIndex: number) => { + // separatorIndex sits between tabs[separatorIndex - 1] and tabs[separatorIndex]. + return ( + hoveredTabIndex === separatorIndex - 1 || + hoveredTabIndex === separatorIndex || + activeTabIndex === separatorIndex - 1 || + activeTabIndex === separatorIndex + ); + }, + [hoveredTabIndex, activeTabIndex] + ); const handleTabClick = useCallback( (tab: Tab) => { @@ -98,7 +113,19 @@ export function TabBar({ const onWheel = (e: WheelEvent) => { if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return; - el.scrollLeft += e.deltaY > 0 ? 50 : -50; + + const delta = + e.deltaMode === WheelEvent.DOM_DELTA_LINE + ? e.deltaY * 16 + : e.deltaMode === WheelEvent.DOM_DELTA_PAGE + ? e.deltaY * el.clientWidth + : e.deltaY; + const maxScrollLeft = el.scrollWidth - el.clientWidth; + const nextScrollLeft = Math.min(maxScrollLeft, Math.max(0, el.scrollLeft + delta)); + + if (nextScrollLeft === el.scrollLeft) return; + + el.scrollLeft = nextScrollLeft; e.preventDefault(); }; @@ -146,54 +173,66 @@ export function TabBar({ {leftActions ?
{leftActions}
: null}
- {tabs.map((tab) => { + {tabs.map((tab, index) => { const isActive = tab.id === activeTabId; return ( -
- + {/* Hover-only gradient + close overlay (sidebar pattern) — keeps pill width fixed and avoids ellipsis shift. */} +
+ {/* biome-ignore lint/a11y/useSemanticElements: cannot nest button inside button */} + handleTabClose(e, tab.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleTabClose(e as unknown as React.MouseEvent, tab.id); + } + }} + className="pointer-events-auto rounded-full p-0.5 transition-colors hover:bg-accent hover:text-accent-foreground" + > + + +
+ + ); })} {onNewChat && ( @@ -201,7 +240,7 @@ export function TabBar({ className={cn( // Solid bg + soft left-fade so tabs scrolling underneath the // + button get visually masked into the bar's background. - "sticky right-0 z-10 flex h-full shrink-0 items-center bg-panel pl-3 pr-1", + "sticky right-0 z-10 ml-3 flex h-full shrink-0 items-center bg-panel pl-3 pr-1", "before:content-[''] before:absolute before:inset-y-0 before:-left-4 before:w-4 before:pointer-events-none", "before:bg-gradient-to-r before:from-transparent before:to-panel" )} @@ -209,7 +248,7 @@ export function TabBar({