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.
This commit is contained in:
James Brunton 2025-12-10 17:21:07 +00:00 committed by GitHub
parent b83888c74a
commit 3c92cb7c2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 162 additions and 139 deletions

View File

@ -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<AllToolsNavButtonProps> = ({
handleUnlessSpecialClick(e, handleClick);
};
const iconNode = (
<span className="iconContainer">
<AppsIcon sx={{ fontSize: isActive ? '1.875rem' : '1.5rem' }} />
</span>
);
return (
<Tooltip
content={t("quickAccess.allTools", "Tools")}
@ -68,28 +62,17 @@ const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({
containerStyle={{ marginTop: "-1rem" }}
maxWidth={200}
>
<div className="flex flex-col items-center gap-1 mt-4 mb-2">
<ActionIcon
component="a"
href={navProps.href}
<div className="mt-4 mb-2">
<QuickAccessButton
icon={<AppsIcon sx={{ fontSize: isActive ? '1.875rem' : '1.5rem' }} />}
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}
</ActionIcon>
<span className={`all-tools-text ${isActive ? 'active' : 'inactive'}`}>
{t("quickAccess.allTools", "Tools")}
</span>
href={navProps.href}
ariaLabel={t("quickAccess.allTools", "Tools")}
textClassName="all-tools-text"
component="a"
/>
</div>
</Tooltip>
);

View File

@ -53,16 +53,15 @@ const FitText: React.FC<FitTextProps> = ({
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,
};

View File

@ -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<HTMLDivElement>((_, ref) => {
const [configModalOpen, setConfigModalOpen] = useState(false);
const [activeButton, setActiveButton] = useState<string>('tools');
const scrollableRef = useRef<HTMLDivElement>(null);
const isOverflow = useIsOverflowing(scrollableRef);
const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl';
@ -85,37 +84,27 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
}
};
const buttonStyle = getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView);
// Render navigation button with conditional URL support
return (
<div
key={config.id}
className="flex flex-col items-center gap-1"
style={{ marginTop: index === 0 ? '0.5rem' : "0rem" }}
data-tour={`${config.id}-button`}
>
<ActionIcon
{...(navProps ? {
component: "a" as const,
href: navProps.href,
onClick: (e: React.MouseEvent) => 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`}
>
<span className="iconContainer">
{config.icon}
</span>
</ActionIcon>
<span className={`button-text ${isActive ? 'active' : 'inactive'}`}>
{config.name}
</span>
<QuickAccessButton
icon={config.icon}
label={config.name}
isActive={isActive}
onClick={handleClick}
href={navProps?.href}
ariaLabel={config.name}
backgroundColor={buttonStyle.backgroundColor}
color={buttonStyle.color}
component={navProps ? 'a' : 'button'}
dataTestId={`${config.id}-button`}
dataTour={`${config.id}-button`}
/>
</div>
);
};
@ -150,6 +139,9 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
}
}
},
];
const middleButtons: ButtonConfig[] = [
{
id: 'files',
name: t("quickAccess.files", "Files"),
@ -160,8 +152,6 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
onClick: handleFilesButtonClick
},
];
const middleButtons: ButtonConfig[] = [];
//TODO: Activity
//{
// id: 'activity',
@ -211,13 +201,6 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
</div>
{/* Conditional divider when overflowing */}
{isOverflow && (
<Divider
size="xs"
className="overflow-divider"
/>
)}
{/* Scrollable content area */}
<div
@ -230,7 +213,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
>
<div className="scrollable-content">
{/* Main navigation section */}
<Stack gap="lg" align="center">
<Stack gap="lg" align="stretch">
{mainButtons.map((config, index) => (
<React.Fragment key={config.id}>
{renderNavButton(config, index, config.id === 'read' || config.id === 'automate')}
@ -238,14 +221,6 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
))}
</Stack>
{/* Divider after main buttons (creates gap) */}
{middleButtons.length === 0 && (
<Divider
size="xs"
className="content-divider"
/>
)}
{/* Middle section */}
{middleButtons.length > 0 && (
<>
@ -253,7 +228,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
size="xs"
className="content-divider"
/>
<Stack gap="lg" align="center">
<Stack gap="lg" align="stretch">
{middleButtons.map((config, index) => (
<React.Fragment key={config.id}>
{renderNavButton(config, index)}
@ -267,7 +242,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
<div className="spacer" />
{/* Bottom section */}
<Stack gap="lg" align="center">
<Stack gap="lg" align="stretch">
{bottomButtons.map((buttonConfig, index) => {
// Handle help button with menu or direct action
if (buttonConfig.id === 'help') {

View File

@ -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<ActiveToolButtonProps> = ({ setActiveButton, to
className="button-text active current-tool-label"
/>
</div>
<Divider
size="xs"
className="current-tool-divider"
/>
</div>
)}
</div>

View File

@ -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;
}

View File

@ -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<QuickAccessButtonProps> = ({
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 (
<div className="flex flex-col items-center gap-1" data-tour={dataTour}>
<ActionIcon
{...actionIconProps}
size={buttonSize}
variant="subtle"
style={{
backgroundColor: bgColor,
color: textColor,
border: 'none',
borderRadius: '8px',
textDecoration: 'none',
}}
className={className || (isActive ? 'activeIconScale' : '')}
data-testid={dataTestId}
>
<span className="iconContainer">{icon}</span>
</ActionIcon>
<div style={{ width: '100%' }}>
<FitText
as="span"
text={label}
lines={2}
minimumFontScale={0.5}
className={`${textClassName} ${isActive ? 'active' : 'inactive'}`}
style={{
fontSize: '0.75rem',
textAlign: 'center',
display: 'block',
}}
/>
</div>
</div>
);
};
export default QuickAccessButton;