From 3c92cb7c2be29d1300ab28f765942f57c4502f86 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Wed, 10 Dec 2025 17:21:07 +0000 Subject: [PATCH] Improve styling of quick access bar (#5197) # Description of Changes Currently, the Quick Access Bar only renders well in Chrome. It's got all sorts of layout issues in Firefox and Safari. This PR attempts to retain the recent changes to make the bar thinner etc. but make it work better in all browsers. --- .../components/shared/AllToolsNavButton.tsx | 39 +++----- .../src/core/components/shared/FitText.tsx | 17 ++-- .../core/components/shared/QuickAccessBar.tsx | 71 +++++---------- .../quickAccessBar/ActiveToolButton.tsx | 6 +- .../shared/quickAccessBar/QuickAccessBar.css | 78 ++++++---------- .../quickAccessBar/QuickAccessButton.tsx | 90 +++++++++++++++++++ 6 files changed, 162 insertions(+), 139 deletions(-) create mode 100644 frontend/src/core/components/shared/quickAccessBar/QuickAccessButton.tsx diff --git a/frontend/src/core/components/shared/AllToolsNavButton.tsx b/frontend/src/core/components/shared/AllToolsNavButton.tsx index efa9a2a5d..1608d5bd3 100644 --- a/frontend/src/core/components/shared/AllToolsNavButton.tsx +++ b/frontend/src/core/components/shared/AllToolsNavButton.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { ActionIcon } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { Tooltip } from '@app/components/shared/Tooltip'; import AppsIcon from '@mui/icons-material/AppsRounded'; @@ -7,6 +6,7 @@ import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext'; import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation'; import { handleUnlessSpecialClick } from '@app/utils/clickHandlers'; +import QuickAccessButton from '@app/components/shared/quickAccessBar/QuickAccessButton'; interface AllToolsNavButtonProps { activeButton: string; @@ -54,12 +54,6 @@ const AllToolsNavButton: React.FC = ({ handleUnlessSpecialClick(e, handleClick); }; - const iconNode = ( - - - - ); - return ( = ({ containerStyle={{ marginTop: "-1rem" }} maxWidth={200} > -
- + } + label={t("quickAccess.allTools", "Tools")} + isActive={isActive} onClick={handleNavClick} - size={isActive ? 'lg' : 'md'} - variant="subtle" - aria-label={t("quickAccess.allTools", "Tools")} - style={{ - backgroundColor: isActive ? 'var(--icon-tools-bg)' : 'var(--icon-inactive-bg)', - color: isActive ? 'var(--icon-tools-color)' : 'var(--icon-inactive-color)', - border: 'none', - borderRadius: '8px', - textDecoration: 'none' - }} - className={isActive ? 'activeIconScale' : ''} - > - {iconNode} - - - {t("quickAccess.allTools", "Tools")} - + href={navProps.href} + ariaLabel={t("quickAccess.allTools", "Tools")} + textClassName="all-tools-text" + component="a" + />
); diff --git a/frontend/src/core/components/shared/FitText.tsx b/frontend/src/core/components/shared/FitText.tsx index efdb1c226..29abe6e0c 100644 --- a/frontend/src/core/components/shared/FitText.tsx +++ b/frontend/src/core/components/shared/FitText.tsx @@ -53,16 +53,15 @@ const FitText: React.FC = ({ const clampStyles: CSSProperties = { // Multi-line clamp with ellipsis fallback whiteSpace: lines === 1 ? 'nowrap' : 'normal', - overflow: 'visible', + overflow: 'hidden', textOverflow: 'ellipsis', - display: lines > 1 ? ('-webkit-box' as any) : undefined, - WebkitBoxOrient: lines > 1 ? ('vertical' as any) : undefined, - WebkitLineClamp: lines > 1 ? (lines as any) : undefined, - lineClamp: lines > 1 ? (lines as any) : undefined, - // Favor shrinking over breaking words; only break at natural spaces or softBreakChars - wordBreak: lines > 1 ? ('keep-all' as any) : ('normal' as any), - overflowWrap: 'normal', - hyphens: 'manual', + display: lines > 1 ? '-webkit-box' : undefined, + WebkitBoxOrient: lines > 1 ? 'vertical' : undefined, + WebkitLineClamp: lines > 1 ? lines : undefined, + // Favor breaking words when necessary to prevent overflow + wordBreak: lines > 1 ? 'break-word' : 'normal', + overflowWrap: lines > 1 ? 'break-word' : 'normal', + hyphens: lines > 1 ? 'auto' : 'manual', // fontSize expects rem values (e.g., 1.2, 0.9) to scale with global font size fontSize: fontSize ? `${fontSize}rem` : undefined, }; diff --git a/frontend/src/core/components/shared/QuickAccessBar.tsx b/frontend/src/core/components/shared/QuickAccessBar.tsx index 28efd60cb..e81afcda9 100644 --- a/frontend/src/core/components/shared/QuickAccessBar.tsx +++ b/frontend/src/core/components/shared/QuickAccessBar.tsx @@ -1,10 +1,9 @@ import React, { useState, useRef, forwardRef, useEffect } from "react"; -import { ActionIcon, Stack, Divider, Menu, Indicator } from "@mantine/core"; +import { Stack, Divider, Menu, Indicator } from "@mantine/core"; import { useTranslation } from 'react-i18next'; import { useNavigate, useLocation } from 'react-router-dom'; import LocalIcon from '@app/components/shared/LocalIcon'; import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider"; -import { useIsOverflowing } from '@app/hooks/useIsOverflowing'; import { useFilesModalContext } from '@app/contexts/FilesModalContext'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext'; @@ -18,6 +17,7 @@ import AppConfigModal from '@app/components/shared/AppConfigModal'; import { useAppConfig } from '@app/contexts/AppConfigContext'; import { useLicenseAlert } from "@app/hooks/useLicenseAlert"; import { requestStartTour } from '@app/constants/events'; +import QuickAccessButton from '@app/components/shared/quickAccessBar/QuickAccessButton'; import { isNavButtonActive, @@ -41,7 +41,6 @@ const QuickAccessBar = forwardRef((_, ref) => { const [configModalOpen, setConfigModalOpen] = useState(false); const [activeButton, setActiveButton] = useState('tools'); const scrollableRef = useRef(null); - const isOverflow = useIsOverflowing(scrollableRef); const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl'; @@ -85,37 +84,27 @@ const QuickAccessBar = forwardRef((_, ref) => { } }; + const buttonStyle = getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView); + // Render navigation button with conditional URL support return (
- handleClick(e), - 'aria-label': config.name - } : { - onClick: (e: React.MouseEvent) => handleClick(e), - 'aria-label': config.name - })} - size={isActive ? 'lg' : 'md'} - variant="subtle" - style={getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView)} - className={isActive ? 'activeIconScale' : ''} - data-testid={`${config.id}-button`} - > - - {config.icon} - - - - {config.name} - +
); }; @@ -150,6 +139,9 @@ const QuickAccessBar = forwardRef((_, ref) => { } } }, + ]; + + const middleButtons: ButtonConfig[] = [ { id: 'files', name: t("quickAccess.files", "Files"), @@ -160,8 +152,6 @@ const QuickAccessBar = forwardRef((_, ref) => { onClick: handleFilesButtonClick }, ]; - - const middleButtons: ButtonConfig[] = []; //TODO: Activity //{ // id: 'activity', @@ -211,13 +201,6 @@ const QuickAccessBar = forwardRef((_, ref) => { - {/* Conditional divider when overflowing */} - {isOverflow && ( - - )} {/* Scrollable content area */}
((_, ref) => { >
{/* Main navigation section */} - + {mainButtons.map((config, index) => ( {renderNavButton(config, index, config.id === 'read' || config.id === 'automate')} @@ -238,14 +221,6 @@ const QuickAccessBar = forwardRef((_, ref) => { ))} - {/* Divider after main buttons (creates gap) */} - {middleButtons.length === 0 && ( - - )} - {/* Middle section */} {middleButtons.length > 0 && ( <> @@ -253,7 +228,7 @@ const QuickAccessBar = forwardRef((_, ref) => { size="xs" className="content-divider" /> - + {middleButtons.map((config, index) => ( {renderNavButton(config, index)} @@ -267,7 +242,7 @@ const QuickAccessBar = forwardRef((_, ref) => {
{/* Bottom section */} - + {bottomButtons.map((buttonConfig, index) => { // Handle help button with menu or direct action if (buttonConfig.id === 'help') { diff --git a/frontend/src/core/components/shared/quickAccessBar/ActiveToolButton.tsx b/frontend/src/core/components/shared/quickAccessBar/ActiveToolButton.tsx index 55b79b26c..38ed45484 100644 --- a/frontend/src/core/components/shared/quickAccessBar/ActiveToolButton.tsx +++ b/frontend/src/core/components/shared/quickAccessBar/ActiveToolButton.tsx @@ -13,7 +13,7 @@ */ import React, { useEffect, useRef, useState } from 'react'; -import { ActionIcon } from '@mantine/core'; +import { ActionIcon, Divider } from '@mantine/core'; import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext'; @@ -195,6 +195,10 @@ const ActiveToolButton: React.FC = ({ setActiveButton, to className="button-text active current-tool-label" />
+
)}
diff --git a/frontend/src/core/components/shared/quickAccessBar/QuickAccessBar.css b/frontend/src/core/components/shared/quickAccessBar/QuickAccessBar.css index 2c4d3004a..6f0366fbc 100644 --- a/frontend/src/core/components/shared/quickAccessBar/QuickAccessBar.css +++ b/frontend/src/core/components/shared/quickAccessBar/QuickAccessBar.css @@ -38,9 +38,9 @@ /* Main container styles */ .quick-access-bar-main { background-color: var(--bg-muted); - width: 4rem; - min-width: 4rem; - max-width: 4rem; + width: 4.5rem; + min-width: 4.5rem; + max-width: 4.5rem; position: relative; z-index: 10; border-right: 1px solid var(--border-default); @@ -52,9 +52,9 @@ /* Rainbow mode container */ .quick-access-bar-main.rainbow-mode { background-color: var(--bg-muted); - width: 4rem; - min-width: 4rem; - max-width: 4rem; + width: 4.5rem; + min-width: 4.5rem; + max-width: 4.5rem; position: relative; z-index: 10; border-right: 1px solid var(--border-default); @@ -72,7 +72,7 @@ /* Header padding */ .quick-access-header { - padding: 1rem 0.5rem 0.5rem 0.5rem; + padding: 1rem 0.25rem 0.5rem 0.25rem; } .nav-header { @@ -84,14 +84,6 @@ gap: 0.5rem; } -/* Nav header divider */ -.nav-header-divider { - width: 3rem; - border-color: var(--color-gray-300); - margin-top: 0.5rem; - margin-bottom: 1rem; -} - /* All tools text styles */ .all-tools-text { margin-top: 0.75rem; @@ -116,16 +108,15 @@ .overflow-divider { width: 3rem; border-color: var(--color-gray-300); - margin: 0 0.5rem; + margin: 0 auto; + align-self: center; } /* Scrollable content area */ .quick-access-bar { - overflow-x: auto; - overflow-y: auto; - scrollbar-gutter: stable both-edges; - -webkit-overflow-scrolling: touch; - padding: 0 0.5rem 1rem 0.5rem; + overflow-x: hidden; + overflow-y: hidden; + padding: 0 0.25rem 1rem 0.25rem; } /* Scrollable content container */ @@ -143,21 +134,21 @@ text-rendering: optimizeLegibility; font-synthesis: none; text-align: center; - display: block; + width: 100%; } -/* Allow wrapping under the active top indicator; constrain to two lines */ +/* Allow wrapping under the active top indicator; constrain to three lines */ .current-tool-label { white-space: normal; overflow: hidden; - text-overflow: ellipsis; display: -webkit-box; - -webkit-line-clamp: 2; /* show up to two lines */ - line-clamp: 2; + -webkit-line-clamp: 3; /* show up to three lines */ + line-clamp: 3; -webkit-box-orient: vertical; - word-break: keep-all; - overflow-wrap: normal; - hyphens: manual; + word-break: break-all; + width: 100%; + box-sizing: border-box; + text-align: center; } .button-text.active { @@ -174,13 +165,14 @@ .content-divider { width: 3rem; border-color: var(--color-gray-300); - margin: 1rem 0; + margin: 1rem auto; + align-self: center; } /* Spacer */ .spacer { flex: 1; - margin-top: 1rem; + min-height: 1rem; } /* Config button text */ @@ -242,8 +234,6 @@ .current-tool-slot.visible { max-height: 8.25rem; /* icon + up to 3-line label + divider (132px) */ opacity: 1; - border-bottom: 1px solid var(--color-gray-300); - padding-bottom: 0.75rem; /* push border down for spacing */ margin-bottom: 1rem; } @@ -268,27 +258,9 @@ } } -/* Divider that animates growing from top */ +/* Divider under active tool indicator */ .current-tool-divider { width: 3rem; border-color: var(--color-gray-300); - margin: 0.5rem auto 0.5rem auto; - transform-origin: top; - animation: dividerGrowDown 350ms ease-out; - animation-fill-mode: both; -} - -@keyframes dividerGrowDown { - 0% { - transform: scaleY(0); - opacity: 0; - margin-top: 0; - margin-bottom: 0; - } - 100% { - transform: scaleY(1); - opacity: 1; - margin-top: 0.5rem; - margin-bottom: 0.5rem; - } + margin: 0.75rem auto 0; } diff --git a/frontend/src/core/components/shared/quickAccessBar/QuickAccessButton.tsx b/frontend/src/core/components/shared/quickAccessBar/QuickAccessButton.tsx new file mode 100644 index 000000000..c3ff23726 --- /dev/null +++ b/frontend/src/core/components/shared/quickAccessBar/QuickAccessButton.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { ActionIcon } from '@mantine/core'; +import FitText from '@app/components/shared/FitText'; + +interface QuickAccessButtonProps { + icon: React.ReactNode; + label: string; + isActive: boolean; + onClick?: (e: React.MouseEvent) => void; + href?: string; + ariaLabel: string; + textClassName?: 'button-text' | 'all-tools-text'; + backgroundColor?: string; + color?: string; + size?: 'sm' | 'md' | 'lg'; + className?: string; + component?: 'a' | 'button'; + dataTestId?: string; + dataTour?: string; +} + +const QuickAccessButton: React.FC = ({ + icon, + label, + isActive, + onClick, + href, + ariaLabel, + textClassName = 'button-text', + backgroundColor, + color, + size, + className, + component = 'button', + dataTestId, + dataTour, +}) => { + const buttonSize = size || (isActive ? 'lg' : 'md'); + const bgColor = backgroundColor || (isActive ? 'var(--icon-tools-bg)' : 'var(--icon-inactive-bg)'); + const textColor = color || (isActive ? 'var(--icon-tools-color)' : 'var(--icon-inactive-color)'); + + const actionIconProps = component === 'a' && href + ? { + component: 'a' as const, + href, + onClick, + 'aria-label': ariaLabel, + } + : { + onClick, + 'aria-label': ariaLabel, + }; + + return ( +
+ + {icon} + +
+ +
+
+ ); +}; + +export default QuickAccessButton;