mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
styling changes (#4894)
# Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -68,11 +68,11 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
|
||||
<PageEditorProvider>
|
||||
<SignatureProvider>
|
||||
<RightRailProvider>
|
||||
<TourOrchestrationProvider>
|
||||
<AdminTourOrchestrationProvider>
|
||||
{children}
|
||||
</AdminTourOrchestrationProvider>
|
||||
</TourOrchestrationProvider>
|
||||
<TourOrchestrationProvider>
|
||||
<AdminTourOrchestrationProvider>
|
||||
{children}
|
||||
</AdminTourOrchestrationProvider>
|
||||
</TourOrchestrationProvider>
|
||||
</RightRailProvider>
|
||||
</SignatureProvider>
|
||||
</PageEditorProvider>
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function OnboardingTour() {
|
||||
const stepsConfig: Record<TourStep, StepType> = {
|
||||
[TourStep.ALL_TOOLS]: {
|
||||
selector: '[data-tour="tool-panel"]',
|
||||
content: t('onboarding.allTools', 'This is the <strong>All Tools</strong> panel, where you can browse and select from all available PDF tools.'),
|
||||
content: t('onboarding.allTools', 'This is the <strong>Tools</strong> panel, where you can browse and select from all available PDF tools.'),
|
||||
position: 'center',
|
||||
padding: 0,
|
||||
action: () => {
|
||||
|
||||
@@ -35,20 +35,20 @@ const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({ activeButton, set
|
||||
|
||||
const iconNode = (
|
||||
<span className="iconContainer">
|
||||
<AppsIcon sx={{ fontSize: '2rem' }} />
|
||||
<AppsIcon sx={{ fontSize: isActive ? '1.875rem' : '1.5rem' }} />
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={t("quickAccess.allTools", "All Tools")} position="right" arrow containerStyle={{ marginTop: "-1rem" }} maxWidth={200}>
|
||||
<Tooltip content={t("quickAccess.allTools", "Tools")} position="right" arrow containerStyle={{ marginTop: "-1rem" }} maxWidth={200}>
|
||||
<div className="flex flex-col items-center gap-1 mt-4 mb-2">
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={navProps.href}
|
||||
onClick={handleNavClick}
|
||||
size={'lg'}
|
||||
size={isActive ? 'lg' : 'md'}
|
||||
variant="subtle"
|
||||
aria-label={t("quickAccess.allTools", "All Tools")}
|
||||
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)',
|
||||
@@ -61,7 +61,7 @@ const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({ activeButton, set
|
||||
{iconNode}
|
||||
</ActionIcon>
|
||||
<span className={`all-tools-text ${isActive ? 'active' : 'inactive'}`}>
|
||||
{t("quickAccess.allTools", "All Tools")}
|
||||
{t("quickAccess.allTools", "Tools")}
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Container, Button, Group, useMantineColorScheme } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
@@ -7,6 +7,7 @@ import { useFileHandler } from '@app/hooks/useFileHandler';
|
||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||
import { BASE_PATH } from '@app/constants/app';
|
||||
import { useLogoPath } from '@app/hooks/useLogoPath';
|
||||
import { useFileManager } from '@app/hooks/useFileManager';
|
||||
|
||||
const LandingPage = () => {
|
||||
const { addFiles } = useFileHandler();
|
||||
@@ -16,6 +17,8 @@ const LandingPage = () => {
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
const [isUploadHover, setIsUploadHover] = React.useState(false);
|
||||
const logoPath = useLogoPath();
|
||||
const { loadRecentFiles } = useFileManager();
|
||||
const [hasRecents, setHasRecents] = React.useState<boolean>(false);
|
||||
|
||||
const handleFileDrop = async (files: File[]) => {
|
||||
await addFiles(files);
|
||||
@@ -38,6 +41,22 @@ const LandingPage = () => {
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
// Determine if the user has any recent files (same source as File Manager)
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
const files = await loadRecentFiles();
|
||||
if (isMounted) {
|
||||
setHasRecents((files?.length || 0) > 0);
|
||||
}
|
||||
} catch (_err) {
|
||||
if (isMounted) setHasRecents(false);
|
||||
}
|
||||
})();
|
||||
return () => { isMounted = false; };
|
||||
}, [loadRecentFiles]);
|
||||
|
||||
return (
|
||||
<Container size="70rem" p={0} h="100%" className="flex items-center justify-center" style={{ position: 'relative' }}>
|
||||
{/* White PDF Page Background */}
|
||||
@@ -119,59 +138,89 @@ const LandingPage = () => {
|
||||
}}
|
||||
onMouseLeave={() => setIsUploadHover(false)}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '2rem',
|
||||
height: '38px',
|
||||
paddingLeft: isUploadHover ? 0 : '1rem',
|
||||
paddingRight: isUploadHover ? 0 : '1rem',
|
||||
width: isUploadHover ? '58px' : 'calc(100% - 58px - 0.6rem)',
|
||||
minWidth: isUploadHover ? '58px' : undefined,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease'
|
||||
}}
|
||||
onClick={handleOpenFilesModal}
|
||||
onMouseEnter={() => setIsUploadHover(false)}
|
||||
>
|
||||
<LocalIcon icon="add" width="1.5rem" height="1.5rem" className="text-[var(--accent-interactive)]" />
|
||||
{!isUploadHover && (
|
||||
<span>
|
||||
{t('landing.addFiles', 'Add Files')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '1rem',
|
||||
height: '38px',
|
||||
width: isUploadHover ? 'calc(100% - 50px)' : '58px',
|
||||
minWidth: '58px',
|
||||
paddingLeft: isUploadHover ? '1rem' : 0,
|
||||
paddingRight: isUploadHover ? '1rem' : 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease'
|
||||
}}
|
||||
onClick={handleNativeUploadClick}
|
||||
onMouseEnter={() => setIsUploadHover(true)}
|
||||
>
|
||||
<LocalIcon icon="upload" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
{isUploadHover && (
|
||||
{/* Show both buttons only when recents exist; otherwise show a single Upload button */}
|
||||
{hasRecents && (
|
||||
<>
|
||||
<Button
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '2rem',
|
||||
height: '38px',
|
||||
paddingLeft: isUploadHover ? 0 : '1rem',
|
||||
paddingRight: isUploadHover ? 0 : '1rem',
|
||||
width: isUploadHover ? '58px' : 'calc(100% - 58px - 0.6rem)',
|
||||
minWidth: isUploadHover ? '58px' : undefined,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease'
|
||||
}}
|
||||
onClick={handleOpenFilesModal}
|
||||
onMouseEnter={() => setIsUploadHover(false)}
|
||||
>
|
||||
<LocalIcon icon="add" width="1.5rem" height="1.5rem" className="text-[var(--accent-interactive)]" />
|
||||
{!isUploadHover && (
|
||||
<span>
|
||||
{t('landing.addFiles', 'Add Files')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '1rem',
|
||||
height: '38px',
|
||||
width: isUploadHover ? 'calc(100% - 50px)' : '58px',
|
||||
minWidth: '58px',
|
||||
paddingLeft: isUploadHover ? '1rem' : 0,
|
||||
paddingRight: isUploadHover ? '1rem' : 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease'
|
||||
}}
|
||||
onClick={handleNativeUploadClick}
|
||||
onMouseEnter={() => setIsUploadHover(true)}
|
||||
>
|
||||
<LocalIcon icon="upload" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
{isUploadHover && (
|
||||
<span style={{ marginLeft: '.5rem' }}>
|
||||
{t('landing.uploadFromComputer', 'Upload from computer')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!hasRecents && (
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '1rem',
|
||||
height: '38px',
|
||||
width: '100%',
|
||||
minWidth: '58px',
|
||||
paddingLeft: '1rem',
|
||||
paddingRight: '1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
onClick={handleNativeUploadClick}
|
||||
>
|
||||
<LocalIcon icon="upload" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
<span style={{ marginLeft: '.5rem' }}>
|
||||
{t('landing.uploadFromComputer', 'Upload from computer')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hidden file input for native file picker */}
|
||||
|
||||
@@ -16,6 +16,7 @@ import ActiveToolButton from "@app/components/shared/quickAccessBar/ActiveToolBu
|
||||
import AppConfigModal from '@app/components/shared/AppConfigModal';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import { useOnboarding } from '@app/contexts/OnboardingContext';
|
||||
|
||||
import {
|
||||
isNavButtonActive,
|
||||
getNavButtonStyle,
|
||||
@@ -88,7 +89,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
onClick: () => handleClick(),
|
||||
'aria-label': config.name
|
||||
})}
|
||||
size={isActive ? (config.size || 'lg') : 'lg'}
|
||||
size={isActive ? 'lg' : 'md'}
|
||||
variant="subtle"
|
||||
style={getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView)}
|
||||
className={isActive ? 'activeIconScale' : ''}
|
||||
@@ -108,9 +109,9 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
const mainButtons: ButtonConfig[] = [
|
||||
{
|
||||
id: 'read',
|
||||
name: t("quickAccess.read", "Read"),
|
||||
icon: <LocalIcon icon="menu-book-rounded" width="1.5rem" height="1.5rem" />,
|
||||
size: 'lg',
|
||||
name: t("quickAccess.reader", "Reader"),
|
||||
icon: <LocalIcon icon="menu-book-rounded" width="1.25rem" height="1.25rem" />,
|
||||
size: 'md',
|
||||
isRound: false,
|
||||
type: 'navigation',
|
||||
onClick: () => {
|
||||
@@ -118,23 +119,11 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
handleReaderToggle();
|
||||
}
|
||||
},
|
||||
// {
|
||||
// id: 'sign',
|
||||
// name: t("quickAccess.sign", "Sign"),
|
||||
// icon: <LocalIcon icon="signature-rounded" width="1.25rem" height="1.25rem" />,
|
||||
// size: 'lg',
|
||||
// isRound: false,
|
||||
// type: 'navigation',
|
||||
// onClick: () => {
|
||||
// setActiveButton('sign');
|
||||
// handleToolSelect('sign');
|
||||
// }
|
||||
// },
|
||||
{
|
||||
id: 'automate',
|
||||
name: t("quickAccess.automate", "Automate"),
|
||||
icon: <LocalIcon icon="automation-outline" width="1.6rem" height="1.6rem" />,
|
||||
size: 'lg',
|
||||
icon: <LocalIcon icon="automation-outline" width="1.25rem" height="1.25rem" />,
|
||||
size: 'md',
|
||||
isRound: false,
|
||||
type: 'navigation',
|
||||
onClick: () => {
|
||||
@@ -147,37 +136,36 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const middleButtons: ButtonConfig[] = [
|
||||
{
|
||||
id: 'files',
|
||||
name: t("quickAccess.files", "Files"),
|
||||
icon: <LocalIcon icon="folder-rounded" width="1.6rem" height="1.6rem" />,
|
||||
icon: <LocalIcon icon="folder-rounded" width="1.25rem" height="1.25rem" />,
|
||||
isRound: true,
|
||||
size: 'lg',
|
||||
size: 'md',
|
||||
type: 'modal',
|
||||
onClick: handleFilesButtonClick
|
||||
},
|
||||
//TODO: Activity
|
||||
//{
|
||||
// id: 'activity',
|
||||
// name: t("quickAccess.activity", "Activity"),
|
||||
// icon: <LocalIcon icon="vital-signs-rounded" width="1.25rem" height="1.25rem" />,
|
||||
// isRound: true,
|
||||
// size: 'lg',
|
||||
// type: 'navigation',
|
||||
// onClick: () => setActiveButton('activity')
|
||||
//},
|
||||
];
|
||||
|
||||
const middleButtons: ButtonConfig[] = [];
|
||||
//TODO: Activity
|
||||
//{
|
||||
// id: 'activity',
|
||||
// name: t("quickAccess.activity", "Activity"),
|
||||
// icon: <LocalIcon icon="vital-signs-rounded" width="1.25rem" height="1.25rem" />,
|
||||
// isRound: true,
|
||||
// size: 'lg',
|
||||
// type: 'navigation',
|
||||
// onClick: () => setActiveButton('activity')
|
||||
//},
|
||||
|
||||
const bottomButtons: ButtonConfig[] = [
|
||||
{
|
||||
id: 'help',
|
||||
name: t("quickAccess.help", "Help"),
|
||||
icon: <LocalIcon icon="help-rounded" width="1.5rem" height="1.5rem" />,
|
||||
icon: <LocalIcon icon="help-rounded" width="1.25rem" height="1.25rem" />,
|
||||
isRound: true,
|
||||
size: 'lg',
|
||||
size: 'md',
|
||||
type: 'action',
|
||||
onClick: () => {
|
||||
// This will be overridden by the wrapper logic
|
||||
@@ -185,9 +173,9 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
name: config?.enableLogin ? t("quickAccess.account", "Account") : t("quickAccess.config", "Config"),
|
||||
icon: config?.enableLogin ? <LocalIcon icon="person-rounded" width="1.25rem" height="1.25rem" /> : <LocalIcon icon="settings-rounded" width="1.25rem" height="1.25rem" />,
|
||||
size: 'lg',
|
||||
name: t("quickAccess.settings", "Settings"),
|
||||
icon: <LocalIcon icon="settings-rounded" width="1.25rem" height="1.25rem" />,
|
||||
size: 'md',
|
||||
type: 'modal',
|
||||
onClick: () => {
|
||||
navigate('/settings/overview');
|
||||
@@ -200,7 +188,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="quick-access"
|
||||
className={`h-screen flex flex-col w-20 quick-access-bar-main ${isRainbowMode ? 'rainbow-mode' : ''}`}
|
||||
className={`h-screen flex flex-col w-16 quick-access-bar-main ${isRainbowMode ? 'rainbow-mode' : ''}`}
|
||||
style={{
|
||||
borderRight: '1px solid var(--border-default)'
|
||||
}}
|
||||
@@ -239,20 +227,30 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* Divider after main buttons */}
|
||||
<Divider
|
||||
size="xs"
|
||||
className="content-divider"
|
||||
/>
|
||||
{/* Divider after main buttons (creates gap) */}
|
||||
{middleButtons.length === 0 && (
|
||||
<Divider
|
||||
size="xs"
|
||||
className="content-divider"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Middle section */}
|
||||
<Stack gap="lg" align="center">
|
||||
{middleButtons.map((config, index) => (
|
||||
<React.Fragment key={config.id}>
|
||||
{renderNavButton(config, index)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
{middleButtons.length > 0 && (
|
||||
<>
|
||||
<Divider
|
||||
size="xs"
|
||||
className="content-divider"
|
||||
/>
|
||||
<Stack gap="lg" align="center">
|
||||
{middleButtons.map((config, index) => (
|
||||
<React.Fragment key={config.id}>
|
||||
{renderNavButton(config, index)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Spacer to push bottom buttons to bottom */}
|
||||
<div className="spacer" />
|
||||
|
||||
@@ -14,6 +14,8 @@ import { ViewerContext } from '@app/contexts/ViewerContext';
|
||||
import { useSignature } from '@app/contexts/SignatureContext';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions';
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
|
||||
import { useSidebarContext } from '@app/contexts/SidebarContext';
|
||||
import { RightRailButtonConfig, RightRailRenderContext, RightRailSection } from '@app/types/rightRail';
|
||||
@@ -39,7 +41,7 @@ export default function RightRail() {
|
||||
const { sidebarRefs } = useSidebarContext();
|
||||
const { t } = useTranslation();
|
||||
const viewerContext = React.useContext(ViewerContext);
|
||||
const { toggleTheme } = useRainbowThemeContext();
|
||||
const { toggleTheme, themeMode } = useRainbowThemeContext();
|
||||
const { buttons, actions, allButtonsDisabled } = useRightRail();
|
||||
|
||||
const { pageEditorFunctions, toolPanelMode, leftPanelView } = useToolWorkflow();
|
||||
@@ -195,7 +197,11 @@ export default function RightRail() {
|
||||
className="right-rail-icon"
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
<LocalIcon icon="contrast" width="1.5rem" height="1.5rem" />
|
||||
{themeMode === 'dark' ? (
|
||||
<LightModeIcon sx={{ fontSize: '1.5rem' }} />
|
||||
) : (
|
||||
<DarkModeIcon sx={{ fontSize: '1.5rem' }} />
|
||||
)}
|
||||
</ActionIcon>,
|
||||
t('rightRail.toggleTheme', 'Toggle Theme')
|
||||
)}
|
||||
|
||||
@@ -2,10 +2,10 @@ import React, { useState, useCallback, useMemo } from "react";
|
||||
import { SegmentedControl, Loader } from "@mantine/core";
|
||||
import { useRainbowThemeContext } from '@app/components/shared/RainbowThemeProvider';
|
||||
import rainbowStyles from '@app/styles/rainbow.module.css';
|
||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||
import GridViewIcon from "@mui/icons-material/GridView";
|
||||
import FolderIcon from "@mui/icons-material/Folder";
|
||||
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
||||
import { LocalIcon } from '@app/components/shared/LocalIcon';
|
||||
import { WorkbenchType, isValidWorkbench } from '@app/types/workbench';
|
||||
import { PageEditorFileDropdown } from '@app/components/shared/PageEditorFileDropdown';
|
||||
import type { CustomWorkbenchViewInstance } from '@app/contexts/ToolWorkflowContext';
|
||||
@@ -55,7 +55,7 @@ const createViewOptions = (
|
||||
{switchingTo === "viewer" ? (
|
||||
<Loader size="sm" />
|
||||
) : (
|
||||
<VisibilityIcon fontSize="medium" />
|
||||
<InsertDriveFileIcon fontSize="medium" />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
@@ -84,7 +84,7 @@ const createViewOptions = (
|
||||
{switchingTo === "pageEditor" ? (
|
||||
<Loader size="sm" />
|
||||
) : (
|
||||
<LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />
|
||||
<GridViewIcon fontSize="medium" />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* ActiveToolButton - Shows the currently selected tool at the top of the Quick Access Bar
|
||||
*
|
||||
* When a user selects a tool from the All Tools list, this component displays the tool's
|
||||
* When a user selects a tool from the Tools list, this component displays the tool's
|
||||
* icon and name at the top of the navigation bar. It provides a quick way to see which
|
||||
* tool is currently active and offers a back button to return to the All Tools list.
|
||||
* tool is currently active and offers a back button to return to the Tools list.
|
||||
*
|
||||
* Features:
|
||||
* - Shows tool icon and name when a tool is selected
|
||||
* - Hover to reveal back arrow for returning to All Tools
|
||||
* - Hover to reveal back arrow for returning to Tools
|
||||
* - Smooth slide-down/slide-up animations
|
||||
* - Only appears for tools that don't have dedicated nav buttons (read, sign, automate)
|
||||
*/
|
||||
@@ -149,7 +149,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
handleBackToTools();
|
||||
});
|
||||
}}
|
||||
size={'xl'}
|
||||
size={'lg'}
|
||||
variant="subtle"
|
||||
onMouseEnter={() => setIsBackHover(true)}
|
||||
onMouseLeave={() => setIsBackHover(false)}
|
||||
@@ -165,7 +165,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
>
|
||||
<span className="iconContainer">
|
||||
{isBackHover ? (
|
||||
<ArrowBackRoundedIcon sx={{ fontSize: '1.5rem' }} />
|
||||
<ArrowBackRoundedIcon sx={{ fontSize: '1.875rem' }} />
|
||||
) : (
|
||||
indicatorTool.icon
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
.activeIconScale {
|
||||
transform: scale(1.3);
|
||||
transform: scale(1);
|
||||
transition: transform 0.2s;
|
||||
z-index: 1;
|
||||
width: calc(1.75rem + 10px) !important;
|
||||
height: calc(1.75rem + 10px) !important;
|
||||
}
|
||||
|
||||
.activeIconScale .iconContainer {
|
||||
width: calc(1.5rem + 10px);
|
||||
height: calc(1.5rem + 10px);
|
||||
}
|
||||
|
||||
.activeIconScale .iconContainer svg,
|
||||
.activeIconScale .iconContainer img {
|
||||
width: calc(1.25rem + 10px) !important;
|
||||
height: calc(1.25rem + 10px) !important;
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
transition: width 0.2s, height 0.2s;
|
||||
}
|
||||
|
||||
/* Action icon styles */
|
||||
@@ -24,9 +38,9 @@
|
||||
/* Main container styles */
|
||||
.quick-access-bar-main {
|
||||
background-color: var(--bg-muted);
|
||||
width: 5rem;
|
||||
min-width: 5rem;
|
||||
max-width: 5rem;
|
||||
width: 4rem;
|
||||
min-width: 4rem;
|
||||
max-width: 4rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -34,9 +48,9 @@
|
||||
/* Rainbow mode container */
|
||||
.quick-access-bar-main.rainbow-mode {
|
||||
background-color: var(--bg-muted);
|
||||
width: 5rem;
|
||||
min-width: 5rem;
|
||||
max-width: 5rem;
|
||||
width: 4rem;
|
||||
min-width: 4rem;
|
||||
max-width: 4rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -57,7 +71,7 @@
|
||||
|
||||
/* Nav header divider */
|
||||
.nav-header-divider {
|
||||
width: 3.75rem;
|
||||
width: 3rem;
|
||||
border-color: var(--color-gray-300);
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
@@ -85,7 +99,7 @@
|
||||
|
||||
/* Overflow divider */
|
||||
.overflow-divider {
|
||||
width: 3.75rem;
|
||||
width: 3rem;
|
||||
border-color: var(--color-gray-300);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
@@ -143,7 +157,7 @@
|
||||
|
||||
/* Content divider */
|
||||
.content-divider {
|
||||
width: 3.75rem;
|
||||
width: 3rem;
|
||||
border-color: var(--color-gray-300);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
@@ -241,7 +255,7 @@
|
||||
|
||||
/* Divider that animates growing from top */
|
||||
.current-tool-divider {
|
||||
width: 3.75rem;
|
||||
width: 3rem;
|
||||
border-color: var(--color-gray-300);
|
||||
margin: 0.5rem auto 0.5rem auto;
|
||||
transform-origin: top;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useRef, useLayoutEffect, useState } from "react";
|
||||
import React, { useMemo, useRef } from "react";
|
||||
import { Box, Stack } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ToolRegistryEntry } from "@app/data/toolsTaxonomy";
|
||||
@@ -8,11 +8,10 @@ import type { SubcategoryGroup } from "@app/hooks/useToolSections";
|
||||
import { useFavoriteToolItems } from "@app/hooks/tools/useFavoriteToolItems";
|
||||
import NoToolsFound from "@app/components/tools/shared/NoToolsFound";
|
||||
import { renderToolButtons } from "@app/components/tools/shared/renderToolButtons";
|
||||
import Badge from "@app/components/shared/Badge";
|
||||
import SubcategoryHeader from "@app/components/tools/shared/SubcategoryHeader";
|
||||
import ToolButton from "@app/components/tools/toolPicker/ToolButton";
|
||||
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
|
||||
import { ToolId } from "@app/types/toolId";
|
||||
import { getSubcategoryLabel } from "@app/data/toolsTaxonomy";
|
||||
|
||||
interface ToolPickerProps {
|
||||
selectedToolKey: string | null;
|
||||
@@ -23,49 +22,8 @@ interface ToolPickerProps {
|
||||
|
||||
const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = false }: ToolPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [quickHeaderHeight, setQuickHeaderHeight] = useState(0);
|
||||
const [allHeaderHeight, setAllHeaderHeight] = useState(0);
|
||||
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
const quickHeaderRef = useRef<HTMLDivElement>(null);
|
||||
const allHeaderRef = useRef<HTMLDivElement>(null);
|
||||
const quickAccessRef = useRef<HTMLDivElement>(null);
|
||||
const allToolsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Keep header heights in sync with any dynamic size changes
|
||||
useLayoutEffect(() => {
|
||||
const update = () => {
|
||||
if (quickHeaderRef.current) {
|
||||
setQuickHeaderHeight(quickHeaderRef.current.offsetHeight || 0);
|
||||
}
|
||||
if (allHeaderRef.current) {
|
||||
setAllHeaderHeight(allHeaderRef.current.offsetHeight || 0);
|
||||
}
|
||||
};
|
||||
|
||||
update();
|
||||
|
||||
// Update on window resize
|
||||
window.addEventListener("resize", update);
|
||||
|
||||
// Update on element resize (e.g., font load, badge count change, zoom)
|
||||
const observers: ResizeObserver[] = [];
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
const observe = (el: HTMLDivElement | null, cb: () => void) => {
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver(() => cb());
|
||||
ro.observe(el);
|
||||
observers.push(ro);
|
||||
};
|
||||
observe(quickHeaderRef.current, update);
|
||||
observe(allHeaderRef.current, update);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", update);
|
||||
observers.forEach(o => o.disconnect());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { sections: visibleSections } = useToolSections(filteredTools);
|
||||
const { favoriteTools, toolRegistry } = useToolWorkflow();
|
||||
@@ -84,31 +42,25 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
|
||||
return items;
|
||||
}, [quickSection]);
|
||||
|
||||
const recommendedCount = useMemo(() => favoriteToolItems.length + recommendedItems.length, [favoriteToolItems.length, recommendedItems.length]);
|
||||
const allSection = useMemo(
|
||||
() => visibleSections.find(s => s.key === 'all'),
|
||||
[visibleSections]
|
||||
);
|
||||
|
||||
const scrollTo = (ref: React.RefObject<HTMLDivElement | null>) => {
|
||||
const container = scrollableRef.current;
|
||||
const target = ref.current;
|
||||
if (container && target) {
|
||||
const stackedOffset = ref === allToolsRef
|
||||
? (quickHeaderHeight + allHeaderHeight)
|
||||
: quickHeaderHeight;
|
||||
const top = target.offsetTop - container.offsetTop - (stackedOffset || 0);
|
||||
container.scrollTo({
|
||||
top: Math.max(0, top),
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Build flat list by subcategory for search mode
|
||||
const emptyFilteredTools: ToolPickerProps['filteredTools'] = [];
|
||||
const effectiveFilteredForSearch: ToolPickerProps['filteredTools'] = isSearching ? filteredTools : emptyFilteredTools;
|
||||
const { searchGroups } = useToolSections(effectiveFilteredForSearch);
|
||||
const headerTextStyle: React.CSSProperties = {
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
padding: "0.5rem 0 0.25rem 0.5rem",
|
||||
textTransform: "none",
|
||||
color: "var(--text-secondary, rgba(0, 0, 0, 0.6))",
|
||||
opacity: 0.7
|
||||
};
|
||||
const toTitleCase = (s: string) =>
|
||||
s.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -141,110 +93,55 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
{quickSection && (
|
||||
<>
|
||||
<div
|
||||
ref={quickHeaderRef}
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 2,
|
||||
borderTop: `0.0625rem solid var(--tool-header-border)`,
|
||||
borderBottom: `0.0625rem solid var(--tool-header-border)`,
|
||||
padding: "0.5rem 1rem",
|
||||
fontWeight: 600,
|
||||
background: "var(--tool-header-bg)",
|
||||
color: "var(--tool-header-text)",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onClick={() => scrollTo(quickAccessRef)}
|
||||
>
|
||||
<span style={{ fontSize: "1rem" }}>{t("toolPicker.quickAccess", "QUICK ACCESS")}</span>
|
||||
<Badge>
|
||||
{recommendedCount}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Box ref={quickAccessRef} w="100%" my="sm">
|
||||
<Stack p="sm" gap="xs">
|
||||
{favoriteToolItems.length > 0 && (
|
||||
<Box w="100%">
|
||||
<SubcategoryHeader label={t('toolPanel.fullscreen.favorites', 'Favourites')} mt={0} />
|
||||
<div>
|
||||
{favoriteToolItems.map(({ id, tool }) => (
|
||||
<ToolButton
|
||||
key={`fav-${id}`}
|
||||
id={id}
|
||||
tool={tool}
|
||||
isSelected={selectedToolKey === id}
|
||||
onSelect={onSelect}
|
||||
hasStars
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
{recommendedItems.length > 0 && (
|
||||
<Box w="100%">
|
||||
<SubcategoryHeader label={t('toolPanel.fullscreen.recommended', 'Recommended')} />
|
||||
<div>
|
||||
{recommendedItems.map(({ id, tool }) => (
|
||||
<ToolButton
|
||||
key={`rec-${id}`}
|
||||
id={id as ToolId}
|
||||
tool={tool}
|
||||
isSelected={selectedToolKey === id}
|
||||
onSelect={onSelect}
|
||||
hasStars
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
{/* Flat list: favorites and recommended first, then all subcategories */}
|
||||
<Stack p="sm" gap="xs">
|
||||
{favoriteToolItems.length > 0 && (
|
||||
<Box w="100%">
|
||||
<div style={headerTextStyle}>
|
||||
{t('toolPanel.fullscreen.favorites', 'Favourites')}
|
||||
</div>
|
||||
<div>
|
||||
{favoriteToolItems.map(({ id, tool }) => (
|
||||
<ToolButton
|
||||
key={`fav-${id}`}
|
||||
id={id}
|
||||
tool={tool}
|
||||
isSelected={selectedToolKey === id}
|
||||
onSelect={onSelect}
|
||||
hasStars
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{allSection && (
|
||||
<>
|
||||
<div
|
||||
ref={allHeaderRef}
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: quickSection ? quickHeaderHeight -1 : 0,
|
||||
zIndex: 2,
|
||||
borderTop: `0.0625rem solid var(--tool-header-border)`,
|
||||
borderBottom: `0.0625rem solid var(--tool-header-border)`,
|
||||
padding: "0.5rem 1rem",
|
||||
fontWeight: 600,
|
||||
background: "var(--tool-header-bg)",
|
||||
color: "var(--tool-header-text)",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
onClick={() => scrollTo(allToolsRef)}
|
||||
>
|
||||
<span style={{ fontSize: "1rem" }}>{t("toolPicker.allTools", "ALL TOOLS")}</span>
|
||||
<Badge>
|
||||
{allSection?.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Box ref={allToolsRef} w="100%">
|
||||
<Stack p="sm" gap="xs">
|
||||
{allSection?.subcategories.map((sc: SubcategoryGroup) =>
|
||||
renderToolButtons(t, sc, selectedToolKey, onSelect, true, false, undefined, true)
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{recommendedItems.length > 0 && (
|
||||
<Box w="100%">
|
||||
<div style={headerTextStyle}>
|
||||
{t('toolPanel.fullscreen.recommended', 'Recommended')}
|
||||
</div>
|
||||
<div>
|
||||
{recommendedItems.map(({ id, tool }) => (
|
||||
<ToolButton
|
||||
key={`rec-${id}`}
|
||||
id={id as ToolId}
|
||||
tool={tool}
|
||||
isSelected={selectedToolKey === id}
|
||||
onSelect={onSelect}
|
||||
hasStars
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
{allSection && allSection.subcategories.map((sc: SubcategoryGroup) => (
|
||||
<Box key={sc.subcategoryId} w="100%">
|
||||
<div style={headerTextStyle}>
|
||||
{toTitleCase(getSubcategoryLabel(t, sc.subcategoryId))}
|
||||
</div>
|
||||
{renderToolButtons(t, sc, selectedToolKey, onSelect, false, false, undefined, true)}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{!quickSection && !allSection && <NoToolsFound />}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function ToolSelector({
|
||||
// If no sections, create a simple group from filtered tools
|
||||
if (baseFilteredTools.length > 0) {
|
||||
return [{
|
||||
name: 'All Tools',
|
||||
name: 'Tools',
|
||||
subcategoryId: 'all' as any,
|
||||
tools: baseFilteredTools.map(([key, tool]) => ({ id: key, tool }))
|
||||
}];
|
||||
|
||||
Reference in New Issue
Block a user