tabs new design (#1352)

Co-authored-by: sawka <mike@commandline.dev>
Co-authored-by: Evan Simkowitz <esimkowitz@users.noreply.github.com>
This commit is contained in:
Red J Adaya 2024-12-04 05:53:27 +08:00 committed by GitHub
parent 90e31dfa48
commit 42cbbcdc2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 140 additions and 101 deletions

View File

@ -51,63 +51,71 @@ const isPopoverContent = (
}; };
const Popover = memo( const Popover = memo(
({ children, className, placement = "bottom-start", offset = 3, onDismiss, middleware }: PopoverProps) => { forwardRef<HTMLDivElement, PopoverProps>(
const [isOpen, setIsOpen] = useState(false); ({ children, className, placement = "bottom-start", offset = 3, onDismiss, middleware }, ref) => {
const [isOpen, setIsOpen] = useState(false);
const handleOpenChange = (open: boolean) => { const handleOpenChange = (open: boolean) => {
setIsOpen(open); setIsOpen(open);
if (!open && onDismiss) { if (!open && onDismiss) {
onDismiss(); onDismiss();
}
};
if (offset === undefined) {
offset = 3;
} }
};
if (offset === undefined) { middleware ??= [];
offset = 3; middleware.push(offsetMiddleware(offset));
const { refs, floatingStyles, context } = useFloating({
placement,
open: isOpen,
onOpenChange: handleOpenChange,
middleware: middleware,
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
const renderChildren = Children.map(children, (child) => {
if (isValidElement(child)) {
if (isPopoverButton(child)) {
return cloneElement(child as any, {
isActive: isOpen,
ref: refs.setReference,
getReferenceProps,
// Do not overwrite onClick
});
}
if (isPopoverContent(child)) {
return isOpen
? cloneElement(child as any, {
ref: refs.setFloating,
style: floatingStyles,
getFloatingProps,
})
: null;
}
}
return child;
});
return (
<div ref={ref} className={clsx("popover", className)}>
{renderChildren}
</div>
);
} }
)
middleware ??= [];
middleware.push(offsetMiddleware(offset));
const { refs, floatingStyles, context } = useFloating({
placement,
open: isOpen,
onOpenChange: handleOpenChange,
middleware: middleware,
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
const renderChildren = Children.map(children, (child) => {
if (isValidElement(child)) {
if (isPopoverButton(child)) {
return cloneElement(child as any, {
isActive: isOpen,
ref: refs.setReference,
getReferenceProps,
// Do not overwrite onClick
});
}
if (isPopoverContent(child)) {
return isOpen
? cloneElement(child as any, {
ref: refs.setFloating,
style: floatingStyles,
getFloatingProps,
})
: null;
}
}
return child;
});
return <div className={clsx("popover", className)}>{renderChildren}</div>;
}
); );
Popover.displayName = "Popover";
interface PopoverButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { interface PopoverButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
isActive?: boolean; isActive?: boolean;
children: React.ReactNode; children: React.ReactNode;

View File

@ -8,12 +8,13 @@ import "./windowdrag.scss";
interface WindowDragProps { interface WindowDragProps {
className?: string; className?: string;
style?: React.CSSProperties;
children?: React.ReactNode; children?: React.ReactNode;
} }
const WindowDrag = forwardRef<HTMLDivElement, WindowDragProps>(({ children, className }, ref) => { const WindowDrag = forwardRef<HTMLDivElement, WindowDragProps>(({ children, className, style }, ref) => {
return ( return (
<div ref={ref} className={clsx(`window-drag`, className)}> <div ref={ref} className={clsx(`window-drag`, className)} style={style}>
{children} {children}
</div> </div>
); );

View File

@ -5,22 +5,44 @@
position: absolute; position: absolute;
width: 130px; width: 130px;
height: calc(100% - 1px); height: calc(100% - 1px);
padding: 6px 3px 0px; padding: 6px 0px 0px;
box-sizing: border-box; box-sizing: border-box;
font-weight: bold; font-weight: bold;
color: var(--secondary-text-color); color: var(--secondary-text-color);
opacity: 0; opacity: 0;
display: flex;
align-items: center;
justify-content: center;
&::after {
content: "";
position: absolute;
left: 0;
width: 1px;
height: 14px;
border-right: 1px solid rgb(from var(--main-text-color) r g b / 0.2);
}
.tab-inner { .tab-inner {
position: relative; position: relative;
width: 100%; width: calc(100% - 6px);
height: 100%; height: 100%;
white-space: nowrap; white-space: nowrap;
border-radius: 6px; border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
border: 0.5px solid rgba(255, 255, 255, 0.08);
} }
&:hover {
& + .tab::after,
&::after {
content: none;
}
.tab-inner {
border-color: transparent;
background: rgb(from var(--main-text-color) r g b / 0.07);
}
}
&.animate { &.animate {
transition: transition:
transform 0.3s ease, transform 0.3s ease,
@ -29,16 +51,25 @@
&.active { &.active {
.tab-inner { .tab-inner {
border: 0.5px solid rgba(255, 255, 255, 0.2); border-color: transparent;
background: radial-gradient(ellipse 92px 32px at bottom, rgba(118, 255, 53, 0.3) 0%, transparent 100%), border-radius: 6px;
rgba(255, 255, 255, 0.2); background: rgb(from var(--main-text-color) r g b / 0.07);
} }
.name { .name {
color: var(--main-text-color); color: var(--main-text-color);
} }
&+.tab::after,
&::after {
content: none;
}
} }
&:first-child::after {
content: none;
}
.name { .name {
position: absolute; position: absolute;
top: 50%; top: 50%;
@ -48,7 +79,7 @@
z-index: var(--zindex-tab-name); z-index: var(--zindex-tab-name);
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25); text-shadow: 0px 0px 4px rgb(from var(--main-bg-color) r g b / 0.25);
overflow: hidden; overflow: hidden;
width: calc(100% - 10px); width: calc(100% - 10px);
text-overflow: ellipsis; text-overflow: ellipsis;
@ -56,7 +87,7 @@
&.focused { &.focused {
outline: none; outline: none;
border: 1px solid rgba(255, 255, 255, 0.179); border: 1px solid rgb(from var(--main-text-color) r g b / 0.179);
padding: 2px 6px; padding: 2px 6px;
border-radius: 2px; border-radius: 2px;
} }
@ -84,7 +115,6 @@
&:hover { &:hover {
color: var(--main-text-color); color: var(--main-text-color);
// background-color: var(--highlight-bg-color);
} }
} }
} }

View File

@ -41,13 +41,12 @@
.dev-label, .dev-label,
.app-menu-button { .app-menu-button {
height: 100%;
font-size: 26px; font-size: 26px;
user-select: none; user-select: none;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 2px 2px 0 0; margin: 6px 6px 0 0;
} }
.app-menu-button { .app-menu-button {

View File

@ -104,8 +104,6 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]); const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
const [draggingTab, setDraggingTab] = useState<string>(); const [draggingTab, setDraggingTab] = useState<string>();
const [tabsLoaded, setTabsLoaded] = useState({}); const [tabsLoaded, setTabsLoaded] = useState({});
// const [scrollable, setScrollable] = useState(false);
// const [tabWidth, setTabWidth] = useState(TAB_DEFAULT_WIDTH);
const [newTabId, setNewTabId] = useState<string | null>(null); const [newTabId, setNewTabId] = useState<string | null>(null);
const tabbarWrapperRef = useRef<HTMLDivElement>(null); const tabbarWrapperRef = useRef<HTMLDivElement>(null);
@ -126,6 +124,9 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
const osInstanceRef = useRef<OverlayScrollbars>(null); const osInstanceRef = useRef<OverlayScrollbars>(null);
const draggerRightRef = useRef<HTMLDivElement>(null); const draggerRightRef = useRef<HTMLDivElement>(null);
const draggerLeftRef = useRef<HTMLDivElement>(null); const draggerLeftRef = useRef<HTMLDivElement>(null);
const workspaceSwitcherRef = useRef<HTMLDivElement>(null);
const devLabelRef = useRef<HTMLDivElement>(null);
const appMenuButtonRef = useRef<HTMLDivElement>(null);
const tabWidthRef = useRef<number>(TAB_DEFAULT_WIDTH); const tabWidthRef = useRef<number>(TAB_DEFAULT_WIDTH);
const scrollableRef = useRef<boolean>(false); const scrollableRef = useRef<boolean>(false);
const updateStatusButtonRef = useRef<HTMLButtonElement>(null); const updateStatusButtonRef = useRef<HTMLButtonElement>(null);
@ -185,33 +186,31 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
const addBtnWidth = addBtnRef.current.getBoundingClientRect().width; const addBtnWidth = addBtnRef.current.getBoundingClientRect().width;
const updateStatusLabelWidth = updateStatusButtonRef.current?.getBoundingClientRect().width ?? 0; const updateStatusLabelWidth = updateStatusButtonRef.current?.getBoundingClientRect().width ?? 0;
const configErrorWidth = configErrorButtonRef.current?.getBoundingClientRect().width ?? 0; const configErrorWidth = configErrorButtonRef.current?.getBoundingClientRect().width ?? 0;
const spaceForTabs = const appMenuButtonWidth = appMenuButtonRef.current?.getBoundingClientRect().width ?? 0;
tabbarWrapperWidth - const workspaceSwitcherWidth = workspaceSwitcherRef.current?.getBoundingClientRect().width ?? 0;
(windowDragLeftWidth + DRAGGER_RIGHT_MIN_WIDTH + addBtnWidth + updateStatusLabelWidth + configErrorWidth); const devLabelWidth = devLabelRef.current?.getBoundingClientRect().width ?? 0;
const nonTabElementsWidth =
windowDragLeftWidth +
DRAGGER_RIGHT_MIN_WIDTH +
addBtnWidth +
updateStatusLabelWidth +
configErrorWidth +
appMenuButtonWidth +
workspaceSwitcherWidth +
devLabelWidth;
const spaceForTabs = tabbarWrapperWidth - nonTabElementsWidth;
const numberOfTabs = tabIds.length; 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;
if (spaceForTabs < totalDefaultTabWidth && spaceForTabs > minTotalTabWidth) { // Compute the ideal width per tab by dividing the available space by the number of tabs
newTabWidth = TAB_MIN_WIDTH; let idealTabWidth = spaceForTabs / numberOfTabs;
} else if (minTotalTabWidth > spaceForTabs) {
// Case where tabs cannot shrink further, make the tab bar scrollable // Apply min/max constraints
newTabWidth = TAB_MIN_WIDTH; idealTabWidth = Math.max(TAB_MIN_WIDTH, Math.min(idealTabWidth, TAB_DEFAULT_WIDTH));
newScrollable = true;
} else if (totalDefaultTabWidth > spaceForTabs) { // Determine if the tab bar needs to be scrollable
// Case where resizing is needed due to limited container width const newScrollable = idealTabWidth * numberOfTabs > spaceForTabs;
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 // Apply the calculated width and position to all tabs
tabRefs.current.forEach((ref, index) => { tabRefs.current.forEach((ref, index) => {
@ -221,20 +220,22 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
} else { } else {
ref.current.classList.remove("animate"); ref.current.classList.remove("animate");
} }
ref.current.style.width = `${newTabWidth}px`; ref.current.style.width = `${idealTabWidth}px`;
ref.current.style.transform = `translate3d(${index * newTabWidth}px,0,0)`; ref.current.style.transform = `translate3d(${index * idealTabWidth}px,0,0)`;
ref.current.style.opacity = "1"; ref.current.style.opacity = "1";
} }
}); });
// Update the state with the new tab width if it has changed // Update the state with the new tab width if it has changed
if (newTabWidth !== tabWidth) { if (idealTabWidth !== tabWidthRef.current) {
tabWidthRef.current = newTabWidth; tabWidthRef.current = idealTabWidth;
} }
// Update the state with the new scrollable state if it has changed // Update the state with the new scrollable state if it has changed
if (newScrollable !== scrollable) { if (newScrollable !== scrollableRef.current) {
scrollableRef.current = newScrollable; scrollableRef.current = newScrollable;
} }
// Initialize/destroy overlay scrollbars // Initialize/destroy overlay scrollbars
if (newScrollable) { if (newScrollable) {
osInstanceRef.current = OverlayScrollbars(tabBarRef.current, { ...(OS_OPTIONS as any) }); osInstanceRef.current = OverlayScrollbars(tabBarRef.current, { ...(OS_OPTIONS as any) });
@ -530,13 +531,13 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
const tabsWrapperWidth = tabIds.length * tabWidthRef.current; const tabsWrapperWidth = tabIds.length * tabWidthRef.current;
const devLabel = isDev() ? ( const devLabel = isDev() ? (
<div className="dev-label"> <div ref={devLabelRef} className="dev-label">
<i className="fa fa-brands fa-dev fa-fw" /> <i className="fa fa-brands fa-dev fa-fw" />
</div> </div>
) : undefined; ) : undefined;
const appMenuButton = const appMenuButton =
PLATFORM !== "darwin" && !settings["window:showmenubar"] ? ( PLATFORM !== "darwin" && !settings["window:showmenubar"] ? (
<div className="app-menu-button" onClick={onEllipsisClick}> <div ref={appMenuButtonRef} className="app-menu-button" onClick={onEllipsisClick}>
<i className="fa fa-ellipsis" /> <i className="fa fa-ellipsis" />
</div> </div>
) : undefined; ) : undefined;
@ -545,7 +546,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
<WindowDrag ref={draggerLeftRef} className="left" /> <WindowDrag ref={draggerLeftRef} className="left" />
{appMenuButton} {appMenuButton}
{devLabel} {devLabel}
<WorkspaceSwitcher></WorkspaceSwitcher> <WorkspaceSwitcher />
<div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize> <div className="tab-bar" ref={tabBarRef} data-overlayscrollbars-initialize>
<div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: `${tabsWrapperWidth}px` }}> <div className="tabs-wrapper" ref={tabsWrapperRef} style={{ width: `${tabsWrapperWidth}px` }}>
{tabIds.map((tabId, index) => { {tabIds.map((tabId, index) => {
@ -572,7 +573,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
<div ref={addBtnRef} className="add-tab-btn" onClick={handleAddTab}> <div ref={addBtnRef} className="add-tab-btn" onClick={handleAddTab}>
<i className="fa fa-solid fa-plus fa-fw" /> <i className="fa fa-solid fa-plus fa-fw" />
</div> </div>
<WindowDrag ref={draggerRightRef} className="right" /> <WindowDrag ref={draggerRightRef} className="right" style={{ minWidth: DRAGGER_RIGHT_MIN_WIDTH }} />
<UpdateStatusBanner buttonRef={updateStatusButtonRef} /> <UpdateStatusBanner buttonRef={updateStatusButtonRef} />
<ConfigErrorIcon buttonRef={configErrorButtonRef} /> <ConfigErrorIcon buttonRef={configErrorButtonRef} />
</div> </div>