mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-30 20:06:30 +01:00
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:
parent
b83888c74a
commit
3c92cb7c2b
@ -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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
Loading…
Reference in New Issue
Block a user