From 04062b455910f11dacb07f1b83b33abd9c2378e1 Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Thu, 1 Jan 2026 14:35:06 +0200 Subject: [PATCH] feat: introduce ScrollableTabs component and useSticky hook, and refactor editor tabs to use the new component --- src/components/organisms/editorTabs.tsx | 68 ++--------------- src/components/organisms/index.ts | 1 + src/components/organisms/scrollableTabs.tsx | 83 +++++++++++++++++++++ src/hooks/index.ts | 1 + src/hooks/useSticky.ts | 23 ++++++ 5 files changed, 114 insertions(+), 62 deletions(-) create mode 100644 src/components/organisms/scrollableTabs.tsx create mode 100644 src/hooks/useSticky.ts diff --git a/src/components/organisms/editorTabs.tsx b/src/components/organisms/editorTabs.tsx index c3fe8a6f42..6831ff74a1 100644 --- a/src/components/organisms/editorTabs.tsx +++ b/src/components/organisms/editorTabs.tsx @@ -34,19 +34,13 @@ import { import { MessageTypes } from "@src/types"; import { Project } from "@src/types/models"; import { navigateToProject } from "@src/utilities/navigation"; -import { - cn, - getPreference, - processBulkCodeFixSuggestions, - generateBulkCodeFixSummary, - abbreviateFilePath, -} from "@utilities"; +import { getPreference, processBulkCodeFixSuggestions, generateBulkCodeFixSummary } from "@utilities"; -import { Button, IconButton, IconSvg, Loader, MermaidDiagram, Spinner, Tab, Typography } from "@components/atoms"; -import { CodeFixDiffEditorModal, FileTabMenu, RenameFileModal } from "@components/organisms"; +import { Button, IconSvg, Loader, MermaidDiagram, Spinner, Typography } from "@components/atoms"; +import { CodeFixDiffEditorModal, FileTabMenu, RenameFileModal, ScrollableTabs } from "@components/organisms"; import { AKRoundLogo } from "@assets/image"; -import { Close, SaveIcon } from "@assets/image/icons"; +import { SaveIcon } from "@assets/image/icons"; import "react-pdf/dist/Page/AnnotationLayer.css"; import "react-pdf/dist/Page/TextLayer.css"; @@ -86,7 +80,7 @@ export const EditorTabs = () => { }, [projectId]); const addToast = useToastStore((state) => state.addToast); - const { openFiles, openFileAsActive, closeOpenedFile } = useFileStore(); + const { openFiles, openFileAsActive } = useFileStore(); const { closeModal } = useModalStore(); const { cursorPositionPerProject, setCursorPosition, selectionPerProject } = useSharedBetweenProjectsStore(); @@ -813,23 +807,6 @@ export const EditorTabs = () => { handleCloseCodeFixModal, ]); - const activeCloseIcon = (fileName: string) => { - const isActiveFile = openFiles[projectId]?.find(({ isActive, name }) => name === fileName && isActive); - - return cn("size-4 p-0.5 opacity-50 hover:bg-gray-1100 group-hover:opacity-100", { - "opacity-100": isActiveFile, - }); - }; - - const handleCloseButtonClick = ( - event: React.MouseEvent, - name: string - ): void => { - event.stopPropagation(); - - closeOpenedFile(name); - }; - const handleTabContextMenu = (event: React.MouseEvent, fileName: string) => { event.preventDefault(); event.stopPropagation(); @@ -845,40 +822,7 @@ export const EditorTabs = () => { {projectId ? ( <>
-
- {projectId - ? openFiles[projectId]?.map(({ name }) => { - return ( -
handleTabContextMenu(e, name)}> - openFileAsActive(name)} - title={name} - value={name} - > - {abbreviateFilePath(name)} - - ) => - handleCloseButtonClick(event, name) - } - > - - - -
- ); - }) - : null} -
+ {openFiles[projectId]?.length ? (
, fileName: string) => void; +} + +export const ScrollableTabs = ({ onTabContextMenu }: ScrollableTabsProps) => { + const { t } = useTranslation("tabs", { keyPrefix: "editor" }); + const { projectId } = useParams(); + const { closeOpenedFile, openFileAsActive, openFiles } = useFileStore(); + + const { ref: activeTabRef } = useSticky(); + const activeEditorFileName = (projectId && openFiles[projectId]?.find(({ isActive }) => isActive)?.name) || ""; + + const activeCloseIcon = (fileName: string) => { + const isActiveFile = openFiles[projectId!]?.find(({ isActive, name }) => name === fileName && isActive); + + return cn("size-4 p-0.5 opacity-50 hover:bg-gray-1100 group-hover:opacity-100", { + "opacity-100": isActiveFile, + }); + }; + + const handleCloseButtonClick = ( + event: React.MouseEvent, + name: string + ): void => { + event.stopPropagation(); + closeOpenedFile(name); + }; + + return ( +
+ {projectId + ? openFiles[projectId]?.map(({ name }) => { + const isActive = name === activeEditorFileName; + + return ( +
onTabContextMenu?.(e, name)} + ref={isActive ? activeTabRef : null} + > + openFileAsActive(name)} + title={name} + value={name} + > + {abbreviateFilePath(name)} + + ) => + handleCloseButtonClick(event, name) + } + > + + + +
+ ); + }) + : null} +
+ ); +}; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 97d719c9bb..4cca998ca1 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -26,3 +26,4 @@ export { useCodeFixSuggestions } from "./useCodeFixSuggestions"; export { useMonacoEditorCleanup } from "./useMonacoEditorCleanup"; export { useProjectFilesVisibility } from "./useProjectFilesVisibility"; export { useAutoRefresh } from "./useAutoRefresh"; +export { useSticky } from "./useSticky"; diff --git a/src/hooks/useSticky.ts b/src/hooks/useSticky.ts new file mode 100644 index 0000000000..9ae1371312 --- /dev/null +++ b/src/hooks/useSticky.ts @@ -0,0 +1,23 @@ +import { useEffect, useRef, useState } from "react"; + +export function useSticky() { + const ref = useRef(null); + + const [isSticky, setIsSticky] = useState(false); + + useEffect(() => { + if (!ref.current) { + return; + } + + const observer = new IntersectionObserver(([event]) => setIsSticky(event.intersectionRatio < 1), { + threshold: [1], + rootMargin: "-1px 0px 0px 0px", + }); + observer.observe(ref.current); + + return () => observer.disconnect(); + }, []); + + return { ref, isSticky }; +}