From 42cbbcdc2a81cbd3deeac750e761fdaf4664f94e Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Wed, 4 Dec 2024 05:53:27 +0800 Subject: [PATCH] tabs new design (#1352) Co-authored-by: sawka Co-authored-by: Evan Simkowitz --- frontend/app/element/popover.tsx | 110 +++++++++++++++------------- frontend/app/element/windowdrag.tsx | 5 +- frontend/app/tab/tab.scss | 52 ++++++++++--- frontend/app/tab/tabbar.scss | 3 +- frontend/app/tab/tabbar.tsx | 71 +++++++++--------- 5 files changed, 140 insertions(+), 101 deletions(-) diff --git a/frontend/app/element/popover.tsx b/frontend/app/element/popover.tsx index 587ade0aa..385f89498 100644 --- a/frontend/app/element/popover.tsx +++ b/frontend/app/element/popover.tsx @@ -51,63 +51,71 @@ const isPopoverContent = ( }; const Popover = memo( - ({ children, className, placement = "bottom-start", offset = 3, onDismiss, middleware }: PopoverProps) => { - const [isOpen, setIsOpen] = useState(false); + forwardRef( + ({ children, className, placement = "bottom-start", offset = 3, onDismiss, middleware }, ref) => { + const [isOpen, setIsOpen] = useState(false); - const handleOpenChange = (open: boolean) => { - setIsOpen(open); - if (!open && onDismiss) { - onDismiss(); + const handleOpenChange = (open: boolean) => { + setIsOpen(open); + if (!open && onDismiss) { + onDismiss(); + } + }; + + if (offset === undefined) { + offset = 3; } - }; - if (offset === undefined) { - offset = 3; + 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 ( +
+ {renderChildren} +
+ ); } - - 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
{renderChildren}
; - } + ) ); +Popover.displayName = "Popover"; + interface PopoverButtonProps extends React.ButtonHTMLAttributes { isActive?: boolean; children: React.ReactNode; diff --git a/frontend/app/element/windowdrag.tsx b/frontend/app/element/windowdrag.tsx index a60d2dfba..472b30ab6 100644 --- a/frontend/app/element/windowdrag.tsx +++ b/frontend/app/element/windowdrag.tsx @@ -8,12 +8,13 @@ import "./windowdrag.scss"; interface WindowDragProps { className?: string; + style?: React.CSSProperties; children?: React.ReactNode; } -const WindowDrag = forwardRef(({ children, className }, ref) => { +const WindowDrag = forwardRef(({ children, className, style }, ref) => { return ( -
+
{children}
); diff --git a/frontend/app/tab/tab.scss b/frontend/app/tab/tab.scss index 12f276dd5..d12f19b49 100644 --- a/frontend/app/tab/tab.scss +++ b/frontend/app/tab/tab.scss @@ -5,22 +5,44 @@ position: absolute; width: 130px; height: calc(100% - 1px); - padding: 6px 3px 0px; + padding: 6px 0px 0px; box-sizing: border-box; font-weight: bold; color: var(--secondary-text-color); 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 { position: relative; - width: 100%; + width: calc(100% - 6px); height: 100%; white-space: nowrap; - border-radius: 6px; - background: rgba(255, 255, 255, 0.05); - border: 0.5px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; } + &:hover { + & + .tab::after, + &::after { + content: none; + } + + .tab-inner { + border-color: transparent; + background: rgb(from var(--main-text-color) r g b / 0.07); + } + } + &.animate { transition: transform 0.3s ease, @@ -29,16 +51,25 @@ &.active { .tab-inner { - border: 0.5px solid rgba(255, 255, 255, 0.2); - background: radial-gradient(ellipse 92px 32px at bottom, rgba(118, 255, 53, 0.3) 0%, transparent 100%), - rgba(255, 255, 255, 0.2); + border-color: transparent; + border-radius: 6px; + background: rgb(from var(--main-text-color) r g b / 0.07); } .name { color: var(--main-text-color); } + + &+.tab::after, + &::after { + content: none; + } } + &:first-child::after { + content: none; + } + .name { position: absolute; top: 50%; @@ -48,7 +79,7 @@ z-index: var(--zindex-tab-name); font-size: 11px; 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; width: calc(100% - 10px); text-overflow: ellipsis; @@ -56,7 +87,7 @@ &.focused { 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; border-radius: 2px; } @@ -84,7 +115,6 @@ &:hover { color: var(--main-text-color); - // background-color: var(--highlight-bg-color); } } } diff --git a/frontend/app/tab/tabbar.scss b/frontend/app/tab/tabbar.scss index dd0f0a3e6..fbff8aff4 100644 --- a/frontend/app/tab/tabbar.scss +++ b/frontend/app/tab/tabbar.scss @@ -41,13 +41,12 @@ .dev-label, .app-menu-button { - height: 100%; font-size: 26px; user-select: none; display: flex; align-items: center; justify-content: center; - margin: 2px 2px 0 0; + margin: 6px 6px 0 0; } .app-menu-button { diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index a88066188..04d387452 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -104,8 +104,6 @@ const TabBar = memo(({ workspace }: TabBarProps) => { const [dragStartPositions, setDragStartPositions] = useState([]); const [draggingTab, setDraggingTab] = useState(); const [tabsLoaded, setTabsLoaded] = useState({}); - // const [scrollable, setScrollable] = useState(false); - // const [tabWidth, setTabWidth] = useState(TAB_DEFAULT_WIDTH); const [newTabId, setNewTabId] = useState(null); const tabbarWrapperRef = useRef(null); @@ -126,6 +124,9 @@ const TabBar = memo(({ workspace }: TabBarProps) => { const osInstanceRef = useRef(null); const draggerRightRef = useRef(null); const draggerLeftRef = useRef(null); + const workspaceSwitcherRef = useRef(null); + const devLabelRef = useRef(null); + const appMenuButtonRef = useRef(null); const tabWidthRef = useRef(TAB_DEFAULT_WIDTH); const scrollableRef = useRef(false); const updateStatusButtonRef = useRef(null); @@ -185,33 +186,31 @@ const TabBar = memo(({ workspace }: TabBarProps) => { const addBtnWidth = addBtnRef.current.getBoundingClientRect().width; const updateStatusLabelWidth = updateStatusButtonRef.current?.getBoundingClientRect().width ?? 0; const configErrorWidth = configErrorButtonRef.current?.getBoundingClientRect().width ?? 0; - const spaceForTabs = - tabbarWrapperWidth - - (windowDragLeftWidth + DRAGGER_RIGHT_MIN_WIDTH + addBtnWidth + updateStatusLabelWidth + configErrorWidth); + const appMenuButtonWidth = appMenuButtonRef.current?.getBoundingClientRect().width ?? 0; + const workspaceSwitcherWidth = workspaceSwitcherRef.current?.getBoundingClientRect().width ?? 0; + 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 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) { - 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; - } + // Compute the ideal width per tab by dividing the available space by the number of tabs + let idealTabWidth = spaceForTabs / numberOfTabs; + + // Apply min/max constraints + idealTabWidth = Math.max(TAB_MIN_WIDTH, Math.min(idealTabWidth, TAB_DEFAULT_WIDTH)); + + // Determine if the tab bar needs to be scrollable + const newScrollable = idealTabWidth * numberOfTabs > spaceForTabs; // Apply the calculated width and position to all tabs tabRefs.current.forEach((ref, index) => { @@ -221,20 +220,22 @@ const TabBar = memo(({ workspace }: TabBarProps) => { } else { ref.current.classList.remove("animate"); } - ref.current.style.width = `${newTabWidth}px`; - ref.current.style.transform = `translate3d(${index * newTabWidth}px,0,0)`; + ref.current.style.width = `${idealTabWidth}px`; + ref.current.style.transform = `translate3d(${index * idealTabWidth}px,0,0)`; ref.current.style.opacity = "1"; } }); // Update the state with the new tab width if it has changed - if (newTabWidth !== tabWidth) { - tabWidthRef.current = newTabWidth; + if (idealTabWidth !== tabWidthRef.current) { + tabWidthRef.current = idealTabWidth; } + // Update the state with the new scrollable state if it has changed - if (newScrollable !== scrollable) { + if (newScrollable !== scrollableRef.current) { scrollableRef.current = newScrollable; } + // Initialize/destroy overlay scrollbars if (newScrollable) { 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 devLabel = isDev() ? ( -
+
) : undefined; const appMenuButton = PLATFORM !== "darwin" && !settings["window:showmenubar"] ? ( -
+
) : undefined; @@ -545,7 +546,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => { {appMenuButton} {devLabel} - +
{tabIds.map((tabId, index) => { @@ -572,7 +573,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
- +