diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b2141beb7..877b5c48a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -39,7 +39,7 @@ }, "devDependencies": { "@playwright/test": "^1.40.0", - "@types/node": "^24.2.0", + "@types/node": "^24.2.1", "@types/react": "^19.1.4", "@types/react-dom": "^19.1.5", "@vitejs/plugin-react": "^4.5.0", @@ -2386,10 +2386,11 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", - "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~7.10.0" } diff --git a/frontend/package.json b/frontend/package.json index b59be58e9..8154a9a1c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -65,7 +65,7 @@ }, "devDependencies": { "@playwright/test": "^1.40.0", - "@types/node": "^24.2.0", + "@types/node": "^24.2.1", "@types/react": "^19.1.4", "@types/react-dom": "^19.1.5", "@vitejs/plugin-react": "^4.5.0", diff --git a/frontend/src/components/fileManager/FileDetails.tsx b/frontend/src/components/fileManager/FileDetails.tsx index 9673d06ad..dcd460644 100644 --- a/frontend/src/components/fileManager/FileDetails.tsx +++ b/frontend/src/components/fileManager/FileDetails.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Stack, Button } from '@mantine/core'; +import { Stack, Button, Box } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useIndexedDBThumbnail } from '../../hooks/useIndexedDBThumbnail'; import { useFileManagerContext } from '../../contexts/FileManagerContext'; -import FilePreview from './FilePreview'; +import FilePreview from '../shared/FilePreview'; import FileInfoCard from './FileInfoCard'; import CompactFileDetails from './CompactFileDetails'; @@ -76,15 +76,18 @@ const FileDetails: React.FC = ({ return ( {/* Section 1: Thumbnail Preview */} - + + + {/* Section 2: File Details */} void; - onNext: () => void; -} - -const FilePreview: React.FC = ({ - currentFile, - thumbnail, - numberOfFiles, - isAnimating, - modalHeight, - onPrevious, - onNext -}) => { - const hasMultipleFiles = numberOfFiles > 1; - // Common style objects - const navigationArrowStyle = { - position: 'absolute' as const, - top: '50%', - transform: 'translateY(-50%)', - zIndex: 10 - }; - - const stackDocumentBaseStyle = { - position: 'absolute' as const, - width: '100%', - height: '100%' - }; - - const animationStyle = { - transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', - transform: isAnimating ? 'scale(0.95) translateX(1.25rem)' : 'scale(1) translateX(0)', - opacity: isAnimating ? 0.7 : 1 - }; - - const mainDocumentShadow = '0 6px 16px rgba(0, 0, 0, 0.2)'; - const stackDocumentShadows = { - back: '0 2px 8px rgba(0, 0, 0, 0.1)', - middle: '0 3px 10px rgba(0, 0, 0, 0.12)' - }; - - return ( - - - {/* Left Navigation Arrow */} - {hasMultipleFiles && ( - - - - )} - - {/* Document Stack Container */} - - {/* Background documents (stack effect) */} - {/* Show 2 shadow pages for 3+ files */} - {numberOfFiles >= 3 && ( - - )} - - {/* Show 1 shadow page for 2+ files */} - {numberOfFiles >= 2 && ( - - )} - - {/* Main document */} - {currentFile && thumbnail ? ( - {currentFile.name} - ) : currentFile ? ( -
- -
- ) : null} -
- - {/* Right Navigation Arrow */} - {hasMultipleFiles && ( - - - - )} -
-
- ); -}; - -export default FilePreview; \ No newline at end of file diff --git a/frontend/src/components/pageEditor/FileThumbnail.tsx b/frontend/src/components/pageEditor/FileThumbnail.tsx index eba9a12c5..c328a350d 100644 --- a/frontend/src/components/pageEditor/FileThumbnail.tsx +++ b/frontend/src/components/pageEditor/FileThumbnail.tsx @@ -4,9 +4,12 @@ import { useTranslation } from 'react-i18next'; import CloseIcon from '@mui/icons-material/Close'; import VisibilityIcon from '@mui/icons-material/Visibility'; import HistoryIcon from '@mui/icons-material/History'; +import PushPinIcon from '@mui/icons-material/PushPin'; +import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined'; import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; import styles from './PageEditor.module.css'; import FileOperationHistory from '../history/FileOperationHistory'; +import { useFileContext } from '../../contexts/FileContext'; interface FileItem { id: string; @@ -66,6 +69,10 @@ const FileThumbnail = ({ }: FileThumbnailProps) => { const { t } = useTranslation(); const [showHistory, setShowHistory] = useState(false); + const { pinnedFiles, pinFile, unpinFile, isFilePinned, activeFiles } = useFileContext(); + + // Find the actual File object that corresponds to this FileItem + const actualFile = activeFiles.find(f => f.name === file.name && f.size === file.size); const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 B'; @@ -301,6 +308,32 @@ const FileThumbnail = ({ + {actualFile && ( + + { + e.stopPropagation(); + if (isFilePinned(actualFile)) { + unpinFile(actualFile); + onSetStatus(`Unpinned ${file.name}`); + } else { + pinFile(actualFile); + onSetStatus(`Pinned ${file.name}`); + } + }} + > + {isFilePinned(actualFile) ? ( + + ) : ( + + )} + + + )} + void; + onPrevious?: () => void; + onNext?: () => void; +} + +const FilePreview: React.FC = ({ + file, + thumbnail, + showStacking = false, + showHoverOverlay = false, + showNavigation = false, + totalFiles = 1, + isAnimating = false, + onFileClick, + onPrevious, + onNext +}) => { + if (!file) return null; + + const hasMultipleFiles = totalFiles > 1; + + // Animation styles + const animationStyle = isAnimating ? { + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + transform: 'scale(0.95) translateX(1.25rem)', + opacity: 0.7 + } : {}; + + // Build the component composition + let content = ( + onFileClick?.(file)} + /> + ); + + // Wrap with hover overlay if needed + if (showHoverOverlay && onFileClick) { + content = {content}; + } + + // Wrap with document stack if needed + if (showStacking) { + content = ( + + {content} + + ); + } + + // Wrap with navigation if needed + if (showNavigation && hasMultipleFiles && onPrevious && onNext) { + content = ( + + {content} + + ); + } + + return ( + + {content} + + ); +}; + +export default FilePreview; \ No newline at end of file diff --git a/frontend/src/components/shared/filePreview/DocumentStack.tsx b/frontend/src/components/shared/filePreview/DocumentStack.tsx new file mode 100644 index 000000000..16168f6c9 --- /dev/null +++ b/frontend/src/components/shared/filePreview/DocumentStack.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Box } from '@mantine/core'; + +export interface DocumentStackProps { + totalFiles: number; + children: React.ReactNode; +} + +const DocumentStack: React.FC = ({ + totalFiles, + children +}) => { + const stackDocumentBaseStyle = { + position: 'absolute' as const, + width: '100%', + height: '100%' + }; + + const stackDocumentShadows = { + back: '0 2px 8px rgba(0, 0, 0, 0.1)', + middle: '0 3px 10px rgba(0, 0, 0, 0.12)' + }; + + return ( + + {/* Background documents (stack effect) */} + {totalFiles >= 3 && ( + + )} + + {totalFiles >= 2 && ( + + )} + + {/* Main document container */} + + {children} + + + ); +}; + +export default DocumentStack; \ No newline at end of file diff --git a/frontend/src/components/shared/filePreview/DocumentThumbnail.tsx b/frontend/src/components/shared/filePreview/DocumentThumbnail.tsx new file mode 100644 index 000000000..661947be2 --- /dev/null +++ b/frontend/src/components/shared/filePreview/DocumentThumbnail.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Box, Center, Image } from '@mantine/core'; +import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; +import { FileWithUrl } from '../../../types/file'; + +export interface DocumentThumbnailProps { + file: File | FileWithUrl | null; + thumbnail?: string | null; + style?: React.CSSProperties; + onClick?: () => void; + children?: React.ReactNode; +} + +const DocumentThumbnail: React.FC = ({ + file, + thumbnail, + style = {}, + onClick, + children +}) => { + if (!file) return null; + + const containerStyle = { + position: 'relative' as const, + cursor: onClick ? 'pointer' : 'default', + transition: 'opacity 0.2s ease', + width: '100%', + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + ...style + }; + + if (thumbnail) { + return ( + + {`Preview + {children} + + ); + } + + return ( + +
+ +
+ {children} +
+ ); +}; + +export default DocumentThumbnail; \ No newline at end of file diff --git a/frontend/src/components/shared/filePreview/HoverOverlay.tsx b/frontend/src/components/shared/filePreview/HoverOverlay.tsx new file mode 100644 index 000000000..f09808982 --- /dev/null +++ b/frontend/src/components/shared/filePreview/HoverOverlay.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Box } from '@mantine/core'; +import VisibilityIcon from '@mui/icons-material/Visibility'; + +export interface HoverOverlayProps { + onMouseEnter?: (e: React.MouseEvent) => void; + onMouseLeave?: (e: React.MouseEvent) => void; + children: React.ReactNode; +} + +const HoverOverlay: React.FC = ({ + onMouseEnter, + onMouseLeave, + children +}) => { + const defaultMouseEnter = (e: React.MouseEvent) => { + const overlay = e.currentTarget.querySelector('.hover-overlay') as HTMLElement; + if (overlay) overlay.style.opacity = '1'; + }; + + const defaultMouseLeave = (e: React.MouseEvent) => { + const overlay = e.currentTarget.querySelector('.hover-overlay') as HTMLElement; + if (overlay) overlay.style.opacity = '0'; + }; + + return ( + + {children} + + {/* Hover overlay */} + + + + + ); +}; + +export default HoverOverlay; \ No newline at end of file diff --git a/frontend/src/components/shared/filePreview/NavigationArrows.tsx b/frontend/src/components/shared/filePreview/NavigationArrows.tsx new file mode 100644 index 000000000..e9fc96719 --- /dev/null +++ b/frontend/src/components/shared/filePreview/NavigationArrows.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Box, ActionIcon } from '@mantine/core'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; + +export interface NavigationArrowsProps { + onPrevious: () => void; + onNext: () => void; + disabled?: boolean; + children: React.ReactNode; +} + +const NavigationArrows: React.FC = ({ + onPrevious, + onNext, + disabled = false, + children +}) => { + const navigationArrowStyle = { + position: 'absolute' as const, + top: '50%', + transform: 'translateY(-50%)', + zIndex: 10 + }; + + return ( + + {/* Left Navigation Arrow */} + + + + + {/* Content */} + + {children} + + + {/* Right Navigation Arrow */} + + + + + ); +}; + +export default NavigationArrows; \ No newline at end of file diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index 2f2e64c78..67617a657 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -39,7 +39,7 @@ export default function ToolPanel() { }`} style={{ width: isPanelVisible ? '20rem' : '0', - padding: isPanelVisible ? '0.5rem' : '0' + padding: '0' }} >
+ + {/* Compression Method */} Compression Method @@ -54,6 +49,7 @@ const CompressSettings = ({ parameters, onParameterChange, disabled = false }: C {/* Quality Adjustment */} {parameters.compressionMethod === 'quality' && ( + Compression Level
setIsSliding(true)} onTouchEnd={() => setIsSliding(false)} disabled={disabled} - style={{ + style={{ width: '100%', height: '6px', borderRadius: '3px', @@ -107,6 +103,8 @@ const CompressSettings = ({ parameters, onParameterChange, disabled = false }: C )} + + {/* File Size Input */} {parameters.compressionMethod === 'filesize' && ( @@ -141,7 +139,7 @@ const CompressSettings = ({ parameters, onParameterChange, disabled = false }: C {/* Compression Options */} -