mirror of
https://github.com/khairul169/code-share.git
synced 2025-04-28 16:49:36 +07:00
137 lines
3.5 KiB
TypeScript
137 lines
3.5 KiB
TypeScript
import { cn } from "~/lib/utils";
|
|
import React, { useEffect, useMemo, useRef } from "react";
|
|
import ActionButton from "./action-button";
|
|
import { FiX } from "react-icons/fi";
|
|
import FileIcon from "./file-icon";
|
|
|
|
export type Tab = {
|
|
title: string;
|
|
icon?: React.ReactNode;
|
|
render?: () => React.ReactNode;
|
|
};
|
|
|
|
type Props = {
|
|
tabs: Tab[];
|
|
current?: number;
|
|
onChange?: (idx: number) => void;
|
|
onClose?: (idx: number) => void;
|
|
};
|
|
|
|
const Tabs = ({ tabs, current = 0, onChange, onClose }: 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]);
|
|
|
|
const tabView = useMemo(() => {
|
|
const tab = tabs[current];
|
|
const element = tab?.render ? tab.render() : null;
|
|
return element;
|
|
}, [tabs, current]);
|
|
|
|
return (
|
|
<div className="w-full h-full flex flex-col items-stretch bg-slate-800">
|
|
{tabs.length > 0 ? (
|
|
<nav
|
|
ref={tabContainerRef}
|
|
className="flex items-stretch overflow-x-auto w-full h-10 min-h-10 hide-scrollbar"
|
|
>
|
|
{tabs.map((tab, idx) => (
|
|
<TabItem
|
|
key={idx}
|
|
index={idx}
|
|
title={tab.title}
|
|
icon={tab.icon}
|
|
isActive={idx === current}
|
|
onSelect={() => onChange && onChange(idx)}
|
|
onClose={() => onClose && onClose(idx)}
|
|
/>
|
|
))}
|
|
</nav>
|
|
) : null}
|
|
|
|
<main className="flex-1 overflow-hidden">{tabView}</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type TabItemProps = {
|
|
index: number;
|
|
title: string;
|
|
icon?: React.ReactNode;
|
|
isActive?: boolean;
|
|
onSelect: () => void;
|
|
onClose: () => void;
|
|
};
|
|
|
|
const TabItem = ({
|
|
index,
|
|
title,
|
|
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 border-b-2 border-transparent truncate flex-shrink-0 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 ? "border-slate-500 text-white" : ""
|
|
)}
|
|
onClick={onSelect}
|
|
>
|
|
<button className="pl-4 pr-0 truncate flex items-center self-stretch">
|
|
<FileIcon
|
|
file={{ isDirectory: false, filename: title }}
|
|
className="mr-1"
|
|
/>
|
|
<span className="truncate">{filename}</span>
|
|
<span>{ext}</span>
|
|
</button>
|
|
<ActionButton
|
|
icon={FiX}
|
|
className="opacity-0 group-hover:opacity-100 transition-colors"
|
|
onClick={onClose}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Tabs;
|