mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-07 19:28:44 +01:00
824a8540ff
This improves the app updater so that it doesn't rely on unreliable system notifications. Now, a banner in the tab bar will display when an update is available. Clicking this will prompt the user to restart the app and complete the installation. This also updates the tab bar to move to the smaller tab size earlier so we don't need to make the tab bar scrollable as much. ![image](https://github.com/user-attachments/assets/79e24617-d609-4554-bdb2-979f810a9b66)
547 lines
21 KiB
TypeScript
547 lines
21 KiB
TypeScript
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import { WindowDrag } from "@/element/windowdrag";
|
|
import { atoms, getApi, isDev } from "@/store/global";
|
|
import * as services from "@/store/services";
|
|
import { deleteLayoutStateAtomForTab } from "frontend/layout/lib/layoutAtom";
|
|
import { useAtomValue } from "jotai";
|
|
import { OverlayScrollbars } from "overlayscrollbars";
|
|
import React, { createRef, useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
import { Tab } from "./tab";
|
|
|
|
import { debounce } from "throttle-debounce";
|
|
import "./tabbar.less";
|
|
|
|
const TAB_DEFAULT_WIDTH = 130;
|
|
const TAB_MIN_WIDTH = 100;
|
|
const DRAGGER_RIGHT_MIN_WIDTH = 74;
|
|
const OS_OPTIONS = {
|
|
overflow: {
|
|
x: "scroll",
|
|
y: "hidden",
|
|
},
|
|
scrollbars: {
|
|
theme: "os-theme-dark",
|
|
visibility: "auto",
|
|
autoHide: "leave",
|
|
autoHideDelay: 1300,
|
|
autoHideSuspend: false,
|
|
dragScroll: true,
|
|
clickScroll: false,
|
|
pointers: ["mouse", "touch", "pen"],
|
|
},
|
|
};
|
|
|
|
interface TabBarProps {
|
|
workspace: Workspace;
|
|
}
|
|
|
|
const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
|
const [tabIds, setTabIds] = useState<string[]>([]);
|
|
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
|
|
const [draggingTab, setDraggingTab] = useState<string>();
|
|
const [tabsLoaded, setTabsLoaded] = useState({});
|
|
// const [scrollable, setScrollable] = useState(false);
|
|
// const [tabWidth, setTabWidth] = useState(TAB_DEFAULT_WIDTH);
|
|
const [newTabId, setNewTabId] = useState<string | null>(null);
|
|
|
|
const tabbarWrapperRef = useRef<HTMLDivElement>(null);
|
|
const tabBarRef = useRef<HTMLDivElement>(null);
|
|
const tabsWrapperRef = useRef<HTMLDivElement>(null);
|
|
const tabRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
|
|
const addBtnRef = useRef<HTMLDivElement>(null);
|
|
const draggingTimeoutIdRef = useRef<NodeJS.Timeout>(null);
|
|
const scrollToNewTabTimeoutIdRef = useRef<NodeJS.Timeout>(null);
|
|
const newTabIdTimeoutIdRef = useRef<NodeJS.Timeout>(null);
|
|
const draggingRemovedRef = useRef(false);
|
|
const draggingTabDataRef = useRef({
|
|
tabId: "",
|
|
ref: { current: null },
|
|
tabStartX: 0,
|
|
tabIndex: 0,
|
|
initialOffsetX: null,
|
|
totalScrollOffset: null,
|
|
dragged: false,
|
|
});
|
|
const osInstanceRef = useRef<OverlayScrollbars>(null);
|
|
const draggerRightRef = useRef<HTMLDivElement>(null);
|
|
const draggerLeftRef = useRef<HTMLDivElement>(null);
|
|
const tabWidthRef = useRef<number>(TAB_DEFAULT_WIDTH);
|
|
const scrollableRef = useRef<boolean>(false);
|
|
const updateStatusLabelRef = useRef<HTMLDivElement>(null);
|
|
|
|
const windowData = useAtomValue(atoms.waveWindow);
|
|
const { activetabid } = windowData;
|
|
|
|
const isFullScreen = useAtomValue(atoms.isFullScreen);
|
|
|
|
const appUpdateStatus = useAtomValue(atoms.updaterStatusAtom);
|
|
|
|
let prevDelta: number;
|
|
let prevDragDirection: string;
|
|
|
|
// Update refs when tabIds change
|
|
useEffect(() => {
|
|
tabRefs.current = tabIds.map((_, index) => tabRefs.current[index] || createRef());
|
|
}, [tabIds]);
|
|
|
|
useEffect(() => {
|
|
if (workspace) {
|
|
// Compare current tabIds with new workspace.tabids
|
|
const currentTabIds = new Set(tabIds);
|
|
const newTabIds = new Set(workspace.tabids);
|
|
|
|
const areEqual =
|
|
currentTabIds.size === newTabIds.size && [...currentTabIds].every((id) => newTabIds.has(id));
|
|
|
|
if (!areEqual) {
|
|
setTabIds(workspace.tabids);
|
|
}
|
|
}
|
|
}, [workspace, tabIds]);
|
|
|
|
const saveTabsPosition = useCallback(() => {
|
|
const tabs = tabRefs.current;
|
|
if (tabs === null) return;
|
|
|
|
const newStartPositions: number[] = [];
|
|
let cumulativeLeft = 0; // Start from the left edge
|
|
|
|
tabRefs.current.forEach((ref) => {
|
|
if (ref.current) {
|
|
newStartPositions.push(cumulativeLeft);
|
|
cumulativeLeft += ref.current.getBoundingClientRect().width; // Add each tab's actual width to the cumulative position
|
|
}
|
|
});
|
|
|
|
setDragStartPositions(newStartPositions);
|
|
}, []);
|
|
|
|
const debouncedSaveTabsPosition = debounce(100, () => saveTabsPosition());
|
|
|
|
const updateSizeAndPosition = (animate?: boolean) => {
|
|
const tabBar = tabBarRef.current;
|
|
if (tabBar === null) return;
|
|
|
|
const tabbarWrapperWidth = tabbarWrapperRef.current.getBoundingClientRect().width;
|
|
const windowDragLeftWidth = draggerLeftRef.current.getBoundingClientRect().width;
|
|
const addBtnWidth = addBtnRef.current.getBoundingClientRect().width;
|
|
const updateStatusLabelWidth = updateStatusLabelRef.current?.getBoundingClientRect().width ?? 0;
|
|
const spaceForTabs =
|
|
tabbarWrapperWidth - (windowDragLeftWidth + DRAGGER_RIGHT_MIN_WIDTH + addBtnWidth + updateStatusLabelWidth);
|
|
|
|
const numberOfTabs = tabIds.length;
|
|
const totalDefaultTabWidth = numberOfTabs * TAB_DEFAULT_WIDTH;
|
|
const minTotalTabWidth = numberOfTabs * TAB_MIN_WIDTH;
|
|
const tabWidth = tabWidthRef.current;
|
|
const scrollable = scrollableRef.current;
|
|
let newTabWidth = tabWidth;
|
|
let newScrollable = scrollable;
|
|
|
|
console.log("spaceForTabs", spaceForTabs, minTotalTabWidth);
|
|
|
|
if (spaceForTabs < totalDefaultTabWidth && spaceForTabs > minTotalTabWidth) {
|
|
newTabWidth = TAB_MIN_WIDTH;
|
|
} else if (minTotalTabWidth > spaceForTabs) {
|
|
// Case where tabs cannot shrink further, make the tab bar scrollable
|
|
newTabWidth = TAB_MIN_WIDTH;
|
|
newScrollable = true;
|
|
} else if (totalDefaultTabWidth > spaceForTabs) {
|
|
// Case where resizing is needed due to limited container width
|
|
newTabWidth = spaceForTabs / numberOfTabs;
|
|
newScrollable = false;
|
|
} else {
|
|
// Case where tabs were previously shrunk or there is enough space for default width tabs
|
|
newTabWidth = TAB_DEFAULT_WIDTH;
|
|
newScrollable = false;
|
|
}
|
|
|
|
// Apply the calculated width and position to all tabs
|
|
tabRefs.current.forEach((ref, index) => {
|
|
if (ref.current) {
|
|
if (animate) {
|
|
ref.current.classList.add("animate");
|
|
} else {
|
|
ref.current.classList.remove("animate");
|
|
}
|
|
ref.current.style.width = `${newTabWidth}px`;
|
|
ref.current.style.transform = `translate3d(${index * newTabWidth}px,0,0)`;
|
|
}
|
|
});
|
|
|
|
// Update the state with the new tab width if it has changed
|
|
if (newTabWidth !== tabWidth) {
|
|
tabWidthRef.current = newTabWidth;
|
|
}
|
|
// Update the state with the new scrollable state if it has changed
|
|
if (newScrollable !== scrollable) {
|
|
scrollableRef.current = newScrollable;
|
|
}
|
|
// Initialize/destroy overlay scrollbars
|
|
if (newScrollable) {
|
|
osInstanceRef.current = OverlayScrollbars(tabBarRef.current, { ...(OS_OPTIONS as any) });
|
|
} else {
|
|
if (osInstanceRef.current) {
|
|
osInstanceRef.current.destroy();
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleResizeTabs = useCallback(() => {
|
|
updateSizeAndPosition();
|
|
debouncedSaveTabsPosition();
|
|
}, [tabIds, newTabId, isFullScreen]);
|
|
|
|
useEffect(() => {
|
|
window.addEventListener("resize", () => handleResizeTabs());
|
|
return () => {
|
|
window.removeEventListener("resize", () => handleResizeTabs());
|
|
};
|
|
}, [handleResizeTabs]);
|
|
|
|
useEffect(() => {
|
|
// Check if all tabs are loaded
|
|
const allLoaded = tabIds.length > 0 && tabIds.every((id) => tabsLoaded[id]);
|
|
if (allLoaded) {
|
|
updateSizeAndPosition(newTabId === null);
|
|
saveTabsPosition();
|
|
}
|
|
}, [tabIds, tabsLoaded, newTabId, handleResizeTabs, saveTabsPosition]);
|
|
|
|
// Make sure timeouts are cleared when component is unmounted
|
|
useEffect(() => {
|
|
return () => {
|
|
if (draggingTimeoutIdRef.current) {
|
|
clearTimeout(draggingTimeoutIdRef.current);
|
|
}
|
|
if (scrollToNewTabTimeoutIdRef.current) {
|
|
clearTimeout(scrollToNewTabTimeoutIdRef.current);
|
|
}
|
|
if (newTabIdTimeoutIdRef.current) {
|
|
clearTimeout(newTabIdTimeoutIdRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const getDragDirection = (currentX: number) => {
|
|
let dragDirection;
|
|
if (currentX - prevDelta > 0) {
|
|
dragDirection = "+";
|
|
} else if (currentX - prevDelta === 0) {
|
|
dragDirection = prevDragDirection;
|
|
} else {
|
|
dragDirection = "-";
|
|
}
|
|
prevDelta = currentX;
|
|
prevDragDirection = dragDirection;
|
|
return dragDirection;
|
|
};
|
|
|
|
const getNewTabIndex = (currentX: number, tabIndex: number, dragDirection: string) => {
|
|
let newTabIndex = tabIndex;
|
|
const tabWidth = tabWidthRef.current;
|
|
if (dragDirection === "+") {
|
|
// Dragging to the right
|
|
for (let i = tabIndex + 1; i < tabIds.length; i++) {
|
|
const otherTabStart = dragStartPositions[i];
|
|
if (currentX + tabWidth > otherTabStart + tabWidth / 2) {
|
|
newTabIndex = i;
|
|
}
|
|
}
|
|
} else {
|
|
// Dragging to the left
|
|
for (let i = tabIndex - 1; i >= 0; i--) {
|
|
const otherTabEnd = dragStartPositions[i] + tabWidth;
|
|
if (currentX < otherTabEnd - tabWidth / 2) {
|
|
newTabIndex = i;
|
|
}
|
|
}
|
|
}
|
|
return newTabIndex;
|
|
};
|
|
|
|
const handleMouseMove = (event: MouseEvent) => {
|
|
const { tabId, ref, tabStartX } = draggingTabDataRef.current;
|
|
|
|
let initialOffsetX = draggingTabDataRef.current.initialOffsetX;
|
|
let totalScrollOffset = draggingTabDataRef.current.totalScrollOffset;
|
|
if (initialOffsetX === null) {
|
|
initialOffsetX = event.clientX - tabStartX;
|
|
draggingTabDataRef.current.initialOffsetX = initialOffsetX;
|
|
}
|
|
let currentX = event.clientX - initialOffsetX - totalScrollOffset;
|
|
let tabBarRectWidth = tabBarRef.current.getBoundingClientRect().width;
|
|
// for macos, it's offset to make space for the window buttons
|
|
const tabBarRectLeftOffset = tabBarRef.current.getBoundingClientRect().left;
|
|
const incrementDecrement = tabBarRectLeftOffset * 0.05;
|
|
const dragDirection = getDragDirection(currentX);
|
|
const scrollable = scrollableRef.current;
|
|
const tabWidth = tabWidthRef.current;
|
|
|
|
// Scroll the tab bar if the dragged tab overflows the container bounds
|
|
if (scrollable) {
|
|
const { viewport } = osInstanceRef.current.elements();
|
|
const currentScrollLeft = viewport.scrollLeft;
|
|
|
|
if (event.clientX <= tabBarRectLeftOffset) {
|
|
viewport.scrollLeft = Math.max(0, currentScrollLeft - incrementDecrement); // Scroll left
|
|
if (viewport.scrollLeft !== currentScrollLeft) {
|
|
// Only adjust if the scroll actually changed
|
|
draggingTabDataRef.current.totalScrollOffset += currentScrollLeft - viewport.scrollLeft;
|
|
}
|
|
} else if (event.clientX >= tabBarRectWidth + tabBarRectLeftOffset) {
|
|
viewport.scrollLeft = Math.min(viewport.scrollWidth, currentScrollLeft + incrementDecrement); // Scroll right
|
|
if (viewport.scrollLeft !== currentScrollLeft) {
|
|
// Only adjust if the scroll actually changed
|
|
draggingTabDataRef.current.totalScrollOffset -= viewport.scrollLeft - currentScrollLeft;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Re-calculate currentX after potential scroll adjustment
|
|
initialOffsetX = draggingTabDataRef.current.initialOffsetX;
|
|
totalScrollOffset = draggingTabDataRef.current.totalScrollOffset;
|
|
currentX = event.clientX - initialOffsetX - totalScrollOffset;
|
|
|
|
setDraggingTab((prev) => (prev !== tabId ? tabId : prev));
|
|
|
|
// Check if the tab has moved 5 pixels
|
|
if (Math.abs(currentX - tabStartX) >= 50) {
|
|
draggingTabDataRef.current.dragged = true;
|
|
}
|
|
|
|
// Constrain movement within the container bounds
|
|
if (tabBarRef.current) {
|
|
const numberOfTabs = tabIds.length;
|
|
const totalDefaultTabWidth = numberOfTabs * TAB_DEFAULT_WIDTH;
|
|
if (totalDefaultTabWidth < tabBarRectWidth) {
|
|
// Set to the total default tab width if there's vacant space
|
|
tabBarRectWidth = totalDefaultTabWidth;
|
|
} else if (scrollable) {
|
|
// Set to the scrollable width if the tab bar is scrollable
|
|
tabBarRectWidth = tabsWrapperRef.current.scrollWidth;
|
|
}
|
|
|
|
const minLeft = 0;
|
|
const maxRight = tabBarRectWidth - tabWidth;
|
|
|
|
// Adjust currentX to stay within bounds
|
|
currentX = Math.min(Math.max(currentX, minLeft), maxRight);
|
|
}
|
|
|
|
ref.current!.style.transform = `translate3d(${currentX}px,0,0)`;
|
|
ref.current!.style.zIndex = "100";
|
|
|
|
const tabIndex = draggingTabDataRef.current.tabIndex;
|
|
const newTabIndex = getNewTabIndex(currentX, tabIndex, dragDirection);
|
|
|
|
if (newTabIndex !== tabIndex) {
|
|
// Remove the dragged tab if not already done
|
|
if (!draggingRemovedRef.current) {
|
|
tabIds.splice(tabIndex, 1);
|
|
draggingRemovedRef.current = true;
|
|
}
|
|
|
|
// Find current index of the dragged tab in tempTabs
|
|
const currentIndexOfDraggingTab = tabIds.indexOf(tabId);
|
|
|
|
// Move the dragged tab to its new position
|
|
if (currentIndexOfDraggingTab !== -1) {
|
|
tabIds.splice(currentIndexOfDraggingTab, 1);
|
|
}
|
|
tabIds.splice(newTabIndex, 0, tabId);
|
|
|
|
// Update visual positions of the tabs
|
|
tabIds.forEach((localTabId, index) => {
|
|
const ref = tabRefs.current.find((ref) => ref.current.dataset.tabId === localTabId);
|
|
if (ref.current && localTabId !== tabId) {
|
|
ref.current.style.transform = `translate3d(${index * tabWidth}px,0,0)`;
|
|
ref.current.classList.add("animate");
|
|
}
|
|
});
|
|
|
|
draggingTabDataRef.current.tabIndex = newTabIndex;
|
|
}
|
|
};
|
|
|
|
const handleMouseUp = (event: MouseEvent) => {
|
|
const { tabIndex, dragged } = draggingTabDataRef.current;
|
|
|
|
// Update the final position of the dragged tab
|
|
const draggingTab = tabIds[tabIndex];
|
|
const tabWidth = tabWidthRef.current;
|
|
const finalLeftPosition = tabIndex * tabWidth;
|
|
const ref = tabRefs.current.find((ref) => ref.current.dataset.tabId === draggingTab);
|
|
if (ref.current) {
|
|
ref.current.classList.add("animate");
|
|
ref.current.style.transform = `translate3d(${finalLeftPosition}px,0,0)`;
|
|
}
|
|
|
|
if (dragged) {
|
|
draggingTimeoutIdRef.current = setTimeout(() => {
|
|
// Reset styles
|
|
tabRefs.current.forEach((ref) => {
|
|
ref.current.style.zIndex = "0";
|
|
ref.current.classList.remove("animate");
|
|
});
|
|
// Reset dragging state
|
|
setDraggingTab(null);
|
|
// Update workspace tab ids
|
|
services.ObjectService.UpdateWorkspaceTabIds(workspace.oid, tabIds);
|
|
}, 300);
|
|
} else {
|
|
// Reset styles
|
|
tabRefs.current.forEach((ref) => {
|
|
ref.current.style.zIndex = "0";
|
|
ref.current.classList.remove("animate");
|
|
});
|
|
// Reset dragging state
|
|
setDraggingTab(null);
|
|
}
|
|
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
draggingRemovedRef.current = false;
|
|
};
|
|
|
|
const handleDragStart = useCallback(
|
|
(event: React.MouseEvent<HTMLDivElement, MouseEvent>, tabId: string, ref: React.RefObject<HTMLDivElement>) => {
|
|
if (event.button !== 0) return;
|
|
|
|
const tabIndex = tabIds.indexOf(tabId);
|
|
const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab
|
|
|
|
if (ref.current) {
|
|
draggingTabDataRef.current = {
|
|
tabId: ref.current.dataset.tabId,
|
|
ref,
|
|
tabStartX,
|
|
tabIndex,
|
|
initialOffsetX: null,
|
|
totalScrollOffset: 0,
|
|
dragged: false,
|
|
};
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
|
|
if (draggingTimeoutIdRef.current) {
|
|
clearTimeout(draggingTimeoutIdRef.current);
|
|
}
|
|
}
|
|
},
|
|
[tabIds, dragStartPositions]
|
|
);
|
|
|
|
const handleSelectTab = (tabId: string) => {
|
|
if (!draggingTabDataRef.current.dragged) {
|
|
services.ObjectService.SetActiveTab(tabId);
|
|
}
|
|
};
|
|
|
|
const handleAddTab = () => {
|
|
const newTabName = `T${tabIds.length + 1}`;
|
|
services.ObjectService.AddTabToWorkspace(newTabName, true).then((tabId) => {
|
|
setTabIds([...tabIds, tabId]);
|
|
setNewTabId(tabId);
|
|
});
|
|
services.ObjectService.GetObject;
|
|
tabsWrapperRef.current.style.transition;
|
|
tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.1s ease");
|
|
|
|
scrollToNewTabTimeoutIdRef.current = setTimeout(() => {
|
|
if (scrollableRef.current) {
|
|
const { viewport } = osInstanceRef.current.elements();
|
|
viewport.scrollLeft = tabIds.length * tabWidthRef.current;
|
|
}
|
|
}, 30);
|
|
|
|
newTabIdTimeoutIdRef.current = setTimeout(() => {
|
|
setNewTabId(null);
|
|
}, 100);
|
|
};
|
|
|
|
const handleCloseTab = (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, tabId: string) => {
|
|
event?.stopPropagation();
|
|
services.WindowService.CloseTab(tabId);
|
|
tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease");
|
|
deleteLayoutStateAtomForTab(tabId);
|
|
};
|
|
|
|
const handleTabLoaded = useCallback((tabId) => {
|
|
setTabsLoaded((prev) => {
|
|
if (!prev[tabId]) {
|
|
// Only update if the tab isn't already marked as loaded
|
|
return { ...prev, [tabId]: true };
|
|
}
|
|
return prev;
|
|
});
|
|
}, []);
|
|
|
|
const isBeforeActive = (tabId: string) => {
|
|
return tabIds.indexOf(tabId) === tabIds.indexOf(activetabid) - 1;
|
|
};
|
|
|
|
const tabsWrapperWidth = tabIds.length * tabWidthRef.current;
|
|
let devLabel: React.ReactNode = null;
|
|
if (isDev()) {
|
|
devLabel = (
|
|
<div className="dev-label">
|
|
<i className="fa fa-brands fa-dev fa-fw" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function onUpdateAvailableClick() {
|
|
getApi().installAppUpdate();
|
|
}
|
|
|
|
let updateAvailableLabel: React.ReactNode = null;
|
|
if (appUpdateStatus === "ready") {
|
|
updateAvailableLabel = (
|
|
<div ref={updateStatusLabelRef} className="update-available-label" onClick={onUpdateAvailableClick}>
|
|
Update Available: Click to Install
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div ref={tabbarWrapperRef} className="tab-bar-wrapper">
|
|
<WindowDrag ref={draggerLeftRef} className="left" />
|
|
{devLabel}
|
|
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
|
|
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: `${tabsWrapperWidth}px` }}>
|
|
{tabIds.map((tabId, index) => {
|
|
return (
|
|
<Tab
|
|
key={tabId}
|
|
ref={tabRefs.current[index]}
|
|
id={tabId}
|
|
isFirst={index === 0}
|
|
onSelect={() => handleSelectTab(tabId)}
|
|
active={activetabid === tabId}
|
|
onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])}
|
|
onClose={(event) => handleCloseTab(event, tabId)}
|
|
onLoaded={() => handleTabLoaded(tabId)}
|
|
isBeforeActive={isBeforeActive(tabId)}
|
|
isDragging={draggingTab === tabId}
|
|
tabWidth={tabWidthRef.current}
|
|
isNew={tabId === newTabId}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div ref={addBtnRef} className="add-tab-btn" onClick={handleAddTab}>
|
|
<i className="fa fa-solid fa-plus fa-fw" />
|
|
</div>
|
|
<WindowDrag ref={draggerRightRef} className="right" />
|
|
{updateAvailableLabel}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export { TabBar };
|