import { cn } from "~/lib/utils"; import React, { useEffect, useMemo, useRef } from "react"; import ActionButton from "./action-button"; import { FiX } from "react-icons/fi"; export type Tab = { title: string; icon?: React.ReactNode; render?: () => React.ReactNode; locked?: boolean; }; type Props = { tabs: Tab[]; current?: number; onChange?: (idx: number) => void; onClose?: (idx: number) => void; className?: string; }; const Tabs = ({ tabs, current = 0, onChange, onClose, className }: Props) => { const tabContainerRef = useRef<HTMLDivElement>(null); const onWheel = (e: WheelEvent) => { e.cancelable && e.preventDefault(); if (tabContainerRef.current) { tabContainerRef.current.scrollLeft += e.deltaY + e.deltaX; } }; useEffect(() => { if (!tabs.length || !tabContainerRef.current) { return; } const container = tabContainerRef.current; container.addEventListener("wheel", onWheel); return () => { container.removeEventListener("wheel", onWheel); }; }, [tabs]); useEffect(() => { if (!tabs.length) { return; } const container = tabContainerRef.current; const tabEl: any = container?.querySelector(`[data-idx="${current}"]`); if (!container || !tabEl) { return; } const containerRect = container.getBoundingClientRect(); const scrollX = tabEl.offsetLeft - containerRect.left; container.scrollTo({ left: scrollX, behavior: "smooth" }); }, [tabs, current]); return tabs.length > 0 ? ( <nav ref={tabContainerRef} className={cn( "flex items-stretch overflow-x-auto h-10 gap-1 hide-scrollbar", className )} > {tabs.map((tab, idx) => ( <TabItem key={idx} index={idx} title={tab.title} icon={tab.icon} isActive={idx === current} onSelect={() => onChange && onChange(idx)} onClose={!tab.locked && onClose ? () => onClose(idx) : null} /> ))} </nav> ) : null; }; type TabViewProps = { tabs: Tab[]; current: number; className?: string; }; export const TabView = ({ tabs, current, className }: TabViewProps) => { const tabView = useMemo(() => { const tab = tabs[current]; const element = tab?.render ? tab.render() : null; return element; }, [tabs, current]); return <main className={cn("overflow-hidden", className)}>{tabView}</main>; }; type TabItemProps = { index: number; title: string; icon?: React.ReactNode; isActive?: boolean; onSelect: () => void; onClose?: (() => void) | null; }; const TabItem = ({ index, title, icon, isActive, onSelect, onClose, }: TabItemProps) => { const lastDotFile = title.lastIndexOf("."); const ext = title.substring(lastDotFile); const filename = title.substring(0, lastDotFile); return ( <div data-idx={index} className={cn( "group truncate flex-shrink-0 h-full bg-white/10 rounded-lg text-white/70 transition-all hover:text-white text-center max-w-[140px] md:max-w-[180px] text-sm flex items-center gap-0 relative z-[1]", isActive ? "bg-white/20 text-white" : "" )} onClick={onSelect} > <button type="button" className={cn( "pl-4 pr-4 truncate flex items-center self-stretch", onClose ? "pr-0" : "" )} > {icon} <span className={cn("inline-block truncate", icon ? "ml-2" : "")}> {filename} </span> <span>{ext}</span> </button> {onClose ? ( <ActionButton icon={FiX} className="opacity-0 group-hover:opacity-100 transition-colors" onClick={() => onClose()} /> ) : null} </div> ); }; export default Tabs;