587 lines
21 KiB
TypeScript
587 lines
21 KiB
TypeScript
"use client";
|
|
|
|
import React, { useRef, useState, useEffect, useCallback } from "react";
|
|
import { flushSync } from "react-dom";
|
|
import { X, ChevronDown, RotateCw } from "lucide-react";
|
|
import { useTab, TabItem } from "@/contexts/TabContext";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { cn } from "@/lib/utils";
|
|
import { menuScreenApi } from "@/lib/api/screen";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
const TAB_MIN_WIDTH = 190;
|
|
const TAB_MAX_WIDTH = 190;
|
|
const TAB_GAP = 2;
|
|
const OVERFLOW_BUTTON_WIDTH = 48;
|
|
|
|
function getTabDisplayName(tab: TabItem): string {
|
|
if (tab.parentMenuName && tab.screenName) {
|
|
return `${tab.parentMenuName} - ${tab.screenName}`;
|
|
}
|
|
return tab.screenName || `화면 ${tab.screenId}`;
|
|
}
|
|
|
|
export function TabBar() {
|
|
const { tabs, activeTabId, switchTab, closeTab, closeOtherTabs, closeAllTabs, closeTabsToTheLeft, closeTabsToTheRight, updateTabOrder, openTab, refreshTab } = useTab();
|
|
const router = useRouter();
|
|
const tabsContainerRef = useRef<HTMLDivElement>(null);
|
|
const [dragOver, setDragOver] = useState(false);
|
|
const [visibleCount, setVisibleCount] = useState(tabs.length);
|
|
const [tabWidth, setTabWidth] = useState(TAB_MAX_WIDTH);
|
|
const [contextMenu, setContextMenu] = useState<{ tabId: string; x: number; y: number } | null>(null);
|
|
const dragStateRef = useRef<{
|
|
tabId: string;
|
|
startX: number;
|
|
startIndex: number;
|
|
currentIndex: number;
|
|
order: string[]; // 드래그 중 논리적 순서 (tabId 배열)
|
|
isDragging: boolean;
|
|
} | null>(null);
|
|
const menuDragInsertRef = useRef<number>(-1);
|
|
const pendingDropRef = useRef<{ timerId: ReturnType<typeof setTimeout>; finalize: () => void } | null>(null);
|
|
const tabsRef = useRef(tabs);
|
|
tabsRef.current = tabs;
|
|
const newDropTabIdRef = useRef<string | null>(null);
|
|
|
|
const calculateVisibleTabs = useCallback(() => {
|
|
const container = tabsContainerRef.current;
|
|
if (!container) {
|
|
setVisibleCount(tabs.length);
|
|
return;
|
|
}
|
|
|
|
const containerWidth = container.offsetWidth;
|
|
const tabSlotWidth = TAB_MIN_WIDTH + TAB_GAP;
|
|
|
|
// 오버플로우 없이 전부 들어가는지 확인
|
|
const totalIfAll = tabs.length * tabSlotWidth;
|
|
if (totalIfAll <= containerWidth) {
|
|
// 전부 표시, 남는 공간을 균등 분배
|
|
setVisibleCount(tabs.length);
|
|
const w = Math.floor(containerWidth / tabs.length) - TAB_GAP;
|
|
setTabWidth(Math.min(TAB_MAX_WIDTH, Math.max(TAB_MIN_WIDTH, w)));
|
|
return;
|
|
}
|
|
|
|
// 오버플로우 필요: +N 버튼 공간 확보 후 몇 개 들어가는지 계산
|
|
const availableForTabs = containerWidth - OVERFLOW_BUTTON_WIDTH;
|
|
const count = Math.max(1, Math.floor(availableForTabs / tabSlotWidth));
|
|
|
|
setVisibleCount(count);
|
|
const w = Math.floor(availableForTabs / count) - TAB_GAP;
|
|
setTabWidth(Math.min(TAB_MAX_WIDTH, Math.max(TAB_MIN_WIDTH, w)));
|
|
}, [tabs.length]);
|
|
|
|
useEffect(() => {
|
|
calculateVisibleTabs();
|
|
const resizeObserver = new ResizeObserver(calculateVisibleTabs);
|
|
if (tabsContainerRef.current) {
|
|
resizeObserver.observe(tabsContainerRef.current);
|
|
}
|
|
return () => resizeObserver.disconnect();
|
|
}, [calculateVisibleTabs]);
|
|
|
|
useEffect(() => {
|
|
const handleClick = () => setContextMenu(null);
|
|
if (contextMenu) {
|
|
document.addEventListener("click", handleClick);
|
|
return () => document.removeEventListener("click", handleClick);
|
|
}
|
|
}, [contextMenu]);
|
|
|
|
if (tabs.length === 0) {
|
|
return (
|
|
<div className="relative flex h-9 min-w-0 items-end bg-slate-50 px-1 overflow-hidden shadow-[inset_0_-1px_0_0_rgb(226,232,240)]" />
|
|
);
|
|
}
|
|
|
|
const visibleTabs = tabs.slice(0, visibleCount);
|
|
const overflowTabs = tabs.slice(visibleCount);
|
|
const hasOverflow = overflowTabs.length > 0;
|
|
|
|
// 활성 탭이 오버플로우에 있는 경우, 보이는 탭의 마지막을 교체
|
|
const activeInOverflow = overflowTabs.find(t => t.id === activeTabId);
|
|
let displayVisibleTabs = [...visibleTabs];
|
|
let displayOverflowTabs = [...overflowTabs];
|
|
|
|
if (activeInOverflow && visibleTabs.length > 0) {
|
|
const lastVisible = displayVisibleTabs[displayVisibleTabs.length - 1];
|
|
displayVisibleTabs[displayVisibleTabs.length - 1] = activeInOverflow;
|
|
displayOverflowTabs = displayOverflowTabs.filter(t => t.id !== activeInOverflow.id);
|
|
displayOverflowTabs.unshift(lastVisible);
|
|
}
|
|
|
|
const handleContextMenu = (e: React.MouseEvent, tabId: string) => {
|
|
e.preventDefault();
|
|
setContextMenu({ tabId, x: e.clientX, y: e.clientY });
|
|
};
|
|
|
|
const applyDragTransforms = (order: string[], dragId: string, dragDx: number) => {
|
|
const container = tabsContainerRef.current;
|
|
if (!container) return;
|
|
const slotWidth = tabWidth + TAB_GAP;
|
|
const originalOrder = tabsRef.current.map(t => t.id);
|
|
|
|
order.forEach((id, newIdx) => {
|
|
const el = container.querySelector(`[data-tab-id="${id}"]`) as HTMLElement | null;
|
|
if (!el) return;
|
|
const origIdx = originalOrder.indexOf(id);
|
|
if (origIdx === -1) return;
|
|
|
|
if (id === dragId) {
|
|
el.style.transform = `translateX(${dragDx}px)`;
|
|
el.style.zIndex = "50";
|
|
el.style.opacity = "0.85";
|
|
} else {
|
|
const offset = (newIdx - origIdx) * slotWidth;
|
|
el.style.transform = offset !== 0 ? `translateX(${offset}px)` : "";
|
|
el.style.zIndex = "";
|
|
el.style.opacity = "";
|
|
}
|
|
el.style.transition = "none";
|
|
});
|
|
};
|
|
|
|
const clearAllTransforms = () => {
|
|
const container = tabsContainerRef.current;
|
|
if (!container) return;
|
|
container.querySelectorAll("[data-tab-id]").forEach((el) => {
|
|
const h = el as HTMLElement;
|
|
h.style.transform = "";
|
|
h.style.zIndex = "";
|
|
h.style.opacity = "";
|
|
h.style.transition = "";
|
|
});
|
|
};
|
|
|
|
const handleTabMouseDown = (e: React.MouseEvent, tabId: string) => {
|
|
if (e.button === 1) {
|
|
e.preventDefault();
|
|
closeTab(tabId);
|
|
return;
|
|
}
|
|
if (e.button !== 0) return;
|
|
if ((e.target as HTMLElement).closest("button")) return;
|
|
|
|
// 이전 드롭 애니메이션이 진행 중이면 즉시 완료
|
|
if (pendingDropRef.current) {
|
|
clearTimeout(pendingDropRef.current.timerId);
|
|
pendingDropRef.current.finalize();
|
|
pendingDropRef.current = null;
|
|
}
|
|
|
|
const container = tabsContainerRef.current;
|
|
if (!container) return;
|
|
|
|
// finalize() 후 flushSync로 상태가 갱신되었으므로 최신 tabs 사용
|
|
const currentTabs = tabsRef.current;
|
|
const startIndex = currentTabs.findIndex(t => t.id === tabId);
|
|
const startX = e.clientX;
|
|
const slotWidth = tabWidth + TAB_GAP;
|
|
const order = currentTabs.map(t => t.id);
|
|
|
|
dragStateRef.current = {
|
|
tabId,
|
|
startX,
|
|
startIndex,
|
|
currentIndex: startIndex,
|
|
order,
|
|
isDragging: false,
|
|
};
|
|
|
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
const state = dragStateRef.current;
|
|
if (!state) return;
|
|
|
|
const dx = moveEvent.clientX - state.startX;
|
|
if (!state.isDragging && Math.abs(dx) > 4) {
|
|
state.isDragging = true;
|
|
}
|
|
if (!state.isDragging) return;
|
|
|
|
// 마우스 위치로 목표 슬롯 계산
|
|
const containerLeft = container.getBoundingClientRect().left + 4;
|
|
const mouseRelX = moveEvent.clientX - containerLeft;
|
|
const targetSlot = Math.max(0, Math.min(visibleCount - 1, Math.floor(mouseRelX / slotWidth)));
|
|
|
|
// 순서 배열에서 직접 이동 (React 상태 안 건드림)
|
|
if (targetSlot !== state.currentIndex) {
|
|
const [moved] = state.order.splice(state.currentIndex, 1);
|
|
state.order.splice(targetSlot, 0, moved);
|
|
state.currentIndex = targetSlot;
|
|
}
|
|
|
|
// 드래그 탭의 시각적 위치: 원래 슬롯 기준 마우스 이동량
|
|
const visualDx = dx;
|
|
applyDragTransforms(state.order, state.tabId, visualDx);
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
const state = dragStateRef.current;
|
|
dragStateRef.current = null;
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
|
|
if (state && state.isDragging && state.currentIndex !== state.startIndex) {
|
|
const dragEl = container.querySelector(`[data-tab-id="${state.tabId}"]`) as HTMLElement | null;
|
|
const targetOffset = (state.currentIndex - state.startIndex) * slotWidth;
|
|
const newOrder = [...state.order];
|
|
|
|
if (dragEl) {
|
|
dragEl.style.transition = "transform 150ms ease, opacity 150ms ease";
|
|
dragEl.style.transform = `translateX(${targetOffset}px)`;
|
|
dragEl.style.opacity = "1";
|
|
dragEl.style.zIndex = "50";
|
|
}
|
|
|
|
const finalize = () => {
|
|
clearAllTransforms();
|
|
flushSync(() => updateTabOrder(newOrder));
|
|
pendingDropRef.current = null;
|
|
};
|
|
const timerId = setTimeout(finalize, 150);
|
|
pendingDropRef.current = { timerId, finalize };
|
|
} else if (state && state.isDragging) {
|
|
const dragEl = container.querySelector(`[data-tab-id="${state.tabId}"]`) as HTMLElement | null;
|
|
if (dragEl) {
|
|
dragEl.style.transition = "transform 150ms ease, opacity 150ms ease";
|
|
dragEl.style.transform = "translateX(0)";
|
|
dragEl.style.opacity = "1";
|
|
}
|
|
|
|
const finalize = () => {
|
|
clearAllTransforms();
|
|
pendingDropRef.current = null;
|
|
};
|
|
const timerId = setTimeout(finalize, 150);
|
|
pendingDropRef.current = { timerId, finalize };
|
|
} else {
|
|
clearAllTransforms();
|
|
if (state) switchTab(tabId);
|
|
}
|
|
};
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
};
|
|
|
|
const applyMenuDragGap = (insertIdx: number) => {
|
|
const container = tabsContainerRef.current;
|
|
if (!container) return;
|
|
const slotWidth = tabWidth + TAB_GAP;
|
|
|
|
displayVisibleTabs.forEach((tab, i) => {
|
|
const el = container.querySelector(`[data-tab-id="${tab.id}"]`) as HTMLElement | null;
|
|
if (!el) return;
|
|
if (i >= insertIdx) {
|
|
el.style.transform = `translateX(${slotWidth}px)`;
|
|
el.style.transition = "transform 150ms ease";
|
|
} else {
|
|
el.style.transform = "";
|
|
el.style.transition = "transform 150ms ease";
|
|
}
|
|
});
|
|
};
|
|
|
|
const clearMenuDragGap = () => {
|
|
const container = tabsContainerRef.current;
|
|
if (!container) return;
|
|
container.querySelectorAll("[data-tab-id]").forEach((el) => {
|
|
const h = el as HTMLElement;
|
|
h.style.transform = "";
|
|
h.style.transition = "";
|
|
});
|
|
menuDragInsertRef.current = -1;
|
|
};
|
|
|
|
const handleMenuDragOver = (e: React.DragEvent) => {
|
|
if (e.dataTransfer.types.includes("application/tab-menu")) {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = "copy";
|
|
if (!dragOver) setDragOver(true);
|
|
|
|
const container = tabsContainerRef.current;
|
|
if (container) {
|
|
const containerLeft = container.getBoundingClientRect().left + 4;
|
|
const mouseRelX = e.clientX - containerLeft;
|
|
const slotWidth = tabWidth + TAB_GAP;
|
|
const insertIdx = Math.max(0, Math.min(displayVisibleTabs.length, Math.round(mouseRelX / slotWidth)));
|
|
|
|
if (insertIdx !== menuDragInsertRef.current) {
|
|
menuDragInsertRef.current = insertIdx;
|
|
applyMenuDragGap(insertIdx);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleMenuDragLeave = (e: React.DragEvent) => {
|
|
const container = tabsContainerRef.current;
|
|
if (container && !container.contains(e.relatedTarget as Node)) {
|
|
setDragOver(false);
|
|
clearMenuDragGap();
|
|
}
|
|
};
|
|
|
|
const handleMenuDrop = async (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setDragOver(false);
|
|
|
|
const insertIndex = menuDragInsertRef.current >= 0
|
|
? menuDragInsertRef.current
|
|
: undefined;
|
|
|
|
clearMenuDragGap();
|
|
|
|
const raw = e.dataTransfer.getData("application/tab-menu");
|
|
if (!raw) return;
|
|
|
|
try {
|
|
const data = JSON.parse(raw);
|
|
const { menuObjid, menuName, parentMenuName } = data;
|
|
|
|
const assignedScreens = await menuScreenApi.getScreensByMenu(menuObjid);
|
|
if (assignedScreens.length > 0) {
|
|
const firstScreen = assignedScreens[0];
|
|
|
|
const expectedTabId = `tab-${firstScreen.screenId}-${menuObjid || "default"}`;
|
|
const alreadyOpen = tabs.some(t => t.id === expectedTabId);
|
|
|
|
if (!alreadyOpen) {
|
|
newDropTabIdRef.current = expectedTabId;
|
|
}
|
|
|
|
openTab({
|
|
screenId: firstScreen.screenId,
|
|
menuObjid,
|
|
screenName: menuName,
|
|
menuName: menuName,
|
|
parentMenuName: parentMenuName,
|
|
}, insertIndex);
|
|
|
|
const urlParams = new URLSearchParams();
|
|
urlParams.set("menuObjid", menuObjid.toString());
|
|
router.replace(`/screens/${firstScreen.screenId}?${urlParams.toString()}`);
|
|
|
|
if (!alreadyOpen) {
|
|
requestAnimationFrame(() => {
|
|
const container = tabsContainerRef.current;
|
|
if (!container) return;
|
|
const newEl = container.querySelector(`[data-tab-id="${expectedTabId}"]`) as HTMLElement | null;
|
|
if (newEl) {
|
|
newEl.style.transition = "none";
|
|
newEl.style.transform = "scale(0.85)";
|
|
newEl.style.opacity = "0";
|
|
requestAnimationFrame(() => {
|
|
newEl.style.transition = "transform 150ms ease, opacity 150ms ease";
|
|
newEl.style.transform = "scale(1)";
|
|
newEl.style.opacity = "1";
|
|
setTimeout(() => {
|
|
newEl.style.transition = "";
|
|
newEl.style.transform = "";
|
|
newEl.style.opacity = "";
|
|
newDropTabIdRef.current = null;
|
|
}, 160);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn("드래그 드롭 탭 생성 실패:", error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"relative flex h-9 min-w-0 items-end bg-slate-50 px-1 overflow-hidden shadow-[inset_0_-1px_0_0_rgb(226,232,240)]",
|
|
dragOver && "shadow-[inset_0_-2px_0_0_rgb(59,130,246)]",
|
|
)}
|
|
ref={tabsContainerRef}
|
|
onDragOver={handleMenuDragOver}
|
|
onDragLeave={handleMenuDragLeave}
|
|
onDrop={handleMenuDrop}
|
|
>
|
|
{/* 탭 + 오버플로우 버튼이 나란히 배치 */}
|
|
<div className="flex min-w-0 items-end">
|
|
{displayVisibleTabs.map((tab) => {
|
|
const isActive = tab.id === activeTabId;
|
|
const displayName = getTabDisplayName(tab);
|
|
|
|
return (
|
|
<div
|
|
key={tab.id}
|
|
data-tab-id={tab.id}
|
|
style={{ width: `${tabWidth}px` }}
|
|
className={cn(
|
|
"group relative mr-0.5 flex shrink-0 cursor-pointer items-center gap-1 rounded-t-lg border px-3 text-xs font-medium select-none transition-colors",
|
|
isActive
|
|
? "z-10 h-[33px] border-slate-200 border-b-0 bg-white text-slate-900"
|
|
: "h-8 border-transparent text-slate-500 hover:bg-slate-100 hover:text-slate-700",
|
|
)}
|
|
onMouseDown={(e) => handleTabMouseDown(e, tab.id)}
|
|
onDragStart={(e) => e.preventDefault()}
|
|
onContextMenu={(e) => handleContextMenu(e, tab.id)}
|
|
title={displayName}
|
|
>
|
|
<span className="min-w-0 flex-1 truncate">{displayName}</span>
|
|
{isActive && (
|
|
<button
|
|
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm text-slate-400 transition-colors hover:bg-slate-200 hover:text-slate-700"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
refreshTab(tab.id);
|
|
}}
|
|
title="새로고침"
|
|
>
|
|
<RotateCw className="h-2.5 w-2.5" />
|
|
</button>
|
|
)}
|
|
<button
|
|
className={cn(
|
|
"flex h-4 w-4 shrink-0 items-center justify-center rounded-sm transition-colors",
|
|
isActive
|
|
? "text-slate-400 hover:bg-slate-200 hover:text-slate-700"
|
|
: "text-transparent group-hover:text-slate-400 group-hover:hover:bg-slate-200 group-hover:hover:text-slate-700",
|
|
)}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
closeTab(tab.id);
|
|
}}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{/* 오버플로우 드롭다운 - 마지막 탭 바로 옆 */}
|
|
{hasOverflow && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
className="mb-0.5 flex h-7 shrink-0 items-center justify-center gap-0.5 rounded-md px-2 text-xs font-medium text-slate-500 transition-colors hover:bg-slate-200 hover:text-slate-700"
|
|
title={`외 ${displayOverflowTabs.length}개 탭`}
|
|
>
|
|
<span className="text-[10px]">+{displayOverflowTabs.length}</span>
|
|
<ChevronDown className="h-3 w-3" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="max-h-[400px] w-[280px] overflow-y-auto">
|
|
{displayOverflowTabs.map((tab) => {
|
|
const isActive = tab.id === activeTabId;
|
|
|
|
return (
|
|
<DropdownMenuItem
|
|
key={tab.id}
|
|
className={cn(
|
|
"flex cursor-pointer items-center justify-between gap-2",
|
|
isActive && "bg-slate-100 font-semibold",
|
|
)}
|
|
onClick={() => switchTab(tab.id)}
|
|
>
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
{tab.parentMenuName && (
|
|
<span className="text-[10px] text-slate-400">{tab.parentMenuName}</span>
|
|
)}
|
|
<span className="truncate text-xs">{tab.screenName || `화면 ${tab.screenId}`}</span>
|
|
</div>
|
|
<button
|
|
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm text-slate-400 hover:bg-slate-200 hover:text-slate-700"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
closeTab(tab.id);
|
|
}}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</DropdownMenuItem>
|
|
);
|
|
})}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
className="cursor-pointer text-xs text-slate-500"
|
|
onClick={closeAllTabs}
|
|
>
|
|
모든 탭 닫기
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
|
|
{/* 우클릭 컨텍스트 메뉴 */}
|
|
{contextMenu && (
|
|
<div
|
|
className="fixed z-50 min-w-[160px] rounded-md border border-slate-200 bg-white py-1 shadow-lg"
|
|
style={{ left: contextMenu.x, top: contextMenu.y }}
|
|
>
|
|
<button
|
|
className="flex w-full items-center px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-100"
|
|
onClick={() => {
|
|
refreshTab(contextMenu.tabId);
|
|
setContextMenu(null);
|
|
}}
|
|
>
|
|
새로고침
|
|
</button>
|
|
<div className="my-1 border-t border-slate-200" />
|
|
<button
|
|
className="flex w-full items-center px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-100"
|
|
onClick={() => {
|
|
closeTab(contextMenu.tabId);
|
|
setContextMenu(null);
|
|
}}
|
|
>
|
|
탭 닫기
|
|
</button>
|
|
<button
|
|
className="flex w-full items-center px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-100"
|
|
onClick={() => {
|
|
closeOtherTabs(contextMenu.tabId);
|
|
setContextMenu(null);
|
|
}}
|
|
>
|
|
다른 탭 모두 닫기
|
|
</button>
|
|
<button
|
|
className="flex w-full items-center px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-100"
|
|
onClick={() => {
|
|
closeTabsToTheLeft(contextMenu.tabId);
|
|
setContextMenu(null);
|
|
}}
|
|
>
|
|
왼쪽 탭 모두 닫기
|
|
</button>
|
|
<button
|
|
className="flex w-full items-center px-3 py-1.5 text-xs text-slate-700 hover:bg-slate-100"
|
|
onClick={() => {
|
|
closeTabsToTheRight(contextMenu.tabId);
|
|
setContextMenu(null);
|
|
}}
|
|
>
|
|
오른쪽 탭 모두 닫기
|
|
</button>
|
|
<div className="my-1 border-t border-slate-200" />
|
|
<button
|
|
className="flex w-full items-center px-3 py-1.5 text-xs text-red-600 hover:bg-red-50"
|
|
onClick={() => {
|
|
closeAllTabs();
|
|
setContextMenu(null);
|
|
}}
|
|
>
|
|
모든 탭 닫기
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|