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;