diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index 5baa64feb..974ca294c 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -1,3 +1,4 @@ +import React, { useState } from 'react'; import { Box } from '@mantine/core'; import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; @@ -20,11 +21,12 @@ export default function Workbench() { const { isRainbowMode } = useRainbowThemeContext(); // Use context-based hooks to eliminate all prop drilling - const { state } = useFileState(); + const { state, selectors } = useFileState(); const { workbench: currentView } = useNavigationState(); const { actions: navActions } = useNavigationActions(); const setCurrentView = navActions.setWorkbench; - const activeFiles = state.files.ids; + const activeFiles = selectors.getFiles(); + const [activeFileIndex, setActiveFileIndex] = useState(0); const { previewFile, pageEditorFunctions, @@ -95,6 +97,8 @@ export default function Workbench() { setSidebarsVisible={setSidebarsVisible} previewFile={previewFile} onClose={handlePreviewClose} + activeFileIndex={activeFileIndex} + setActiveFileIndex={setActiveFileIndex} /> ); @@ -150,6 +154,12 @@ export default function Workbench() { { + const stub = selectors.getStirlingFileStub(f.fileId); + return { fileId: f.fileId, name: f.name, versionNumber: stub?.versionNumber }; + })} + currentFileIndex={activeFileIndex} + onFileSelect={setActiveFileIndex} /> )} diff --git a/frontend/src/components/shared/FileDropdownMenu.tsx b/frontend/src/components/shared/FileDropdownMenu.tsx new file mode 100644 index 000000000..4360cfe26 --- /dev/null +++ b/frontend/src/components/shared/FileDropdownMenu.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { Menu, Loader, Group, Text } from '@mantine/core'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; + +interface FileDropdownMenuProps { + displayName: string; + activeFiles: Array<{ fileId: string; name: string; versionNumber?: number }>; + currentFileIndex: number; + onFileSelect?: (index: number) => void; + switchingTo?: string | null; + viewOptionStyle: React.CSSProperties; + pillRef?: React.RefObject; +} + +export const FileDropdownMenu: React.FC = ({ + displayName, + activeFiles, + currentFileIndex, + onFileSelect, + switchingTo, + viewOptionStyle, +}) => { + return ( + + +
+ {switchingTo === "viewer" ? ( + + ) : ( + + )} + {displayName} + +
+
+ + {activeFiles.map((file, index) => { + const itemName = file?.name || 'Untitled'; + const itemDisplayName = itemName.length > 50 ? `${itemName.substring(0, 50)}...` : itemName; + const isActive = index === currentFileIndex; + return ( + { + e.stopPropagation(); + onFileSelect?.(index); + }} + className="viewer-file-tab" + {...(isActive && { 'data-active': true })} + style={{ + justifyContent: 'flex-start', + }} + > + + + {itemDisplayName} + + {file.versionNumber && file.versionNumber > 1 && ( + + v{file.versionNumber} + + )} + + + ); + })} + +
+ ); +}; diff --git a/frontend/src/components/shared/TopControls.tsx b/frontend/src/components/shared/TopControls.tsx index 8edae6e72..3eb0f7f41 100644 --- a/frontend/src/components/shared/TopControls.tsx +++ b/frontend/src/components/shared/TopControls.tsx @@ -6,6 +6,7 @@ import VisibilityIcon from "@mui/icons-material/Visibility"; import EditNoteIcon from "@mui/icons-material/EditNote"; import FolderIcon from "@mui/icons-material/Folder"; import { WorkbenchType, isValidWorkbench } from '../../types/workbench'; +import { FileDropdownMenu } from './FileDropdownMenu'; const viewOptionStyle = { @@ -19,16 +20,40 @@ const viewOptionStyle = { // Build view options showing text always -const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => { +const createViewOptions = ( + currentView: WorkbenchType, + switchingTo: WorkbenchType | null, + activeFiles: Array<{ fileId: string; name: string; versionNumber?: number }>, + currentFileIndex: number, + onFileSelect?: (index: number) => void +) => { + const currentFile = activeFiles[currentFileIndex]; + const isInViewer = currentView === 'viewer'; + const fileName = currentFile?.name || ''; + const displayName = isInViewer && fileName + ? (fileName.length > 30 ? `${fileName.substring(0, 30)}...` : fileName) + : 'Viewer'; + const hasMultipleFiles = activeFiles.length > 1; + const showDropdown = isInViewer && hasMultipleFiles; + const viewerOption = { - label: ( + label: showDropdown ? ( + + ) : (
{switchingTo === "viewer" ? ( ) : ( )} - Viewer + {displayName}
), value: "viewer", @@ -83,12 +108,18 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp interface TopControlsProps { currentView: WorkbenchType; setCurrentView: (view: WorkbenchType) => void; + activeFiles?: Array<{ fileId: string; name: string; versionNumber?: number }>; + currentFileIndex?: number; + onFileSelect?: (index: number) => void; } const TopControls = ({ currentView, setCurrentView, - }: TopControlsProps) => { + activeFiles = [], + currentFileIndex = 0, + onFileSelect, +}: TopControlsProps) => { const { isRainbowMode } = useRainbowThemeContext(); const [switchingTo, setSwitchingTo] = useState(null); @@ -118,7 +149,7 @@ const TopControls = ({
void; onClose?: () => void; previewFile?: File | null; + activeFileIndex?: number; + setActiveFileIndex?: (index: number) => void; } const EmbedPdfViewerContent = ({ @@ -29,8 +28,9 @@ const EmbedPdfViewerContent = ({ setSidebarsVisible: _setSidebarsVisible, onClose, previewFile, + activeFileIndex: externalActiveFileIndex, + setActiveFileIndex: externalSetActiveFileIndex, }: EmbedPdfViewerProps) => { - const { t } = useTranslation(); const viewerRef = React.useRef(null); const [isViewerHovered, setIsViewerHovered] = React.useState(false); @@ -70,9 +70,9 @@ const EmbedPdfViewerContent = ({ const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible; // Track which file tab is active - const [activeFileIndex, setActiveFileIndex] = useState(0); - const [tabsExpanded, setTabsExpanded] = useState(true); - const tabsContainerRef = useRef(null); + const [internalActiveFileIndex, setInternalActiveFileIndex] = useState(0); + const activeFileIndex = externalActiveFileIndex ?? internalActiveFileIndex; + const setActiveFileIndex = externalSetActiveFileIndex ?? setInternalActiveFileIndex; const hasInitializedFromSelection = useRef(false); // When viewer opens with a selected file, switch to that file @@ -94,22 +94,6 @@ const EmbedPdfViewerContent = ({ } }, [activeFiles.length, activeFileIndex]); - // Minimize when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (tabsContainerRef.current && !tabsContainerRef.current.contains(event.target as Node)) { - setTabsExpanded(false); - } - }; - - if (tabsExpanded) { - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - } - }, [tabsExpanded]); - // Determine which file to display const currentFile = React.useMemo(() => { if (previewFile) { @@ -287,91 +271,6 @@ const EmbedPdfViewerContent = ({ ) : ( <> - {/* Floating tabs for multiple files */} - {activeFiles.length > 1 && !previewFile && ( - - setTabsExpanded(!tabsExpanded)} - > - - {tabsExpanded ? : } - - {t('viewer.files', 'Files')} ({activeFiles.length}) - - - - - - - setActiveFileIndex(parseInt(value || '0'))} - variant="pills" - orientation="vertical" - classNames={{ - tab: 'viewer-file-tab' - }} - > - - {activeFiles.map((file, index) => { - const stub = selectors.getStirlingFileStub(file.fileId); - const displayName = file.name.length > 25 ? `${file.name.substring(0, 25)}...` : file.name; - - return ( - - - - - {displayName} - - {stub?.versionNumber && stub.versionNumber > 1 && ( - - v{stub.versionNumber} - - )} - - - - ); - })} - - - - - - )} - {/* EmbedPDF Viewer */} { if (!pdfUrl) return []; + // Calculate 3.5rem in pixels dynamically based on root font size + const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); + const viewportGap = rootFontSize * 3.5; + return [ createPluginRegistration(LoaderPluginPackage, { loadingOptions: { @@ -77,7 +81,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur }, }), createPluginRegistration(ViewportPluginPackage, { - viewportGap: 56, // 3.5rem = 56px to match nav pill height + viewportGap, }), createPluginRegistration(ScrollPluginPackage, { strategy: ScrollStrategy.Vertical, diff --git a/frontend/src/components/viewer/LocalEmbedPDFWithAnnotations.tsx b/frontend/src/components/viewer/LocalEmbedPDFWithAnnotations.tsx index af60d8fa8..25efcb7dd 100644 --- a/frontend/src/components/viewer/LocalEmbedPDFWithAnnotations.tsx +++ b/frontend/src/components/viewer/LocalEmbedPDFWithAnnotations.tsx @@ -64,6 +64,10 @@ export function LocalEmbedPDFWithAnnotations({ const plugins = useMemo(() => { if (!pdfUrl) return []; + // Calculate 3.5rem in pixels dynamically based on root font size + const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); + const viewportGap = rootFontSize * 3.5; + return [ createPluginRegistration(LoaderPluginPackage, { loadingOptions: { @@ -75,7 +79,7 @@ export function LocalEmbedPDFWithAnnotations({ }, }), createPluginRegistration(ViewportPluginPackage, { - viewportGap: 10, + viewportGap, }), createPluginRegistration(ScrollPluginPackage, { strategy: ScrollStrategy.Vertical, diff --git a/frontend/src/components/viewer/Viewer.tsx b/frontend/src/components/viewer/Viewer.tsx index ea2403fd0..dab9ff826 100644 --- a/frontend/src/components/viewer/Viewer.tsx +++ b/frontend/src/components/viewer/Viewer.tsx @@ -5,6 +5,8 @@ export interface ViewerProps { setSidebarsVisible: (v: boolean) => void; onClose?: () => void; previewFile?: File | null; + activeFileIndex?: number; + setActiveFileIndex?: (index: number) => void; } const Viewer = (props: ViewerProps) => {