Nav based file select

This commit is contained in:
Reece 2025-10-10 15:57:41 +01:00
parent 5d3710260f
commit 8e8e06628e
7 changed files with 146 additions and 118 deletions

View File

@ -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() {
<TopControls
currentView={currentView}
setCurrentView={setCurrentView}
activeFiles={activeFiles.map(f => {
const stub = selectors.getStirlingFileStub(f.fileId);
return { fileId: f.fileId, name: f.name, versionNumber: stub?.versionNumber };
})}
currentFileIndex={activeFileIndex}
onFileSelect={setActiveFileIndex}
/>
)}

View File

@ -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<HTMLDivElement>;
}
export const FileDropdownMenu: React.FC<FileDropdownMenuProps> = ({
displayName,
activeFiles,
currentFileIndex,
onFileSelect,
switchingTo,
viewOptionStyle,
}) => {
return (
<Menu trigger="click" position="bottom" width="30rem">
<Menu.Target>
<div style={{...viewOptionStyle, cursor: 'pointer'} as React.CSSProperties}>
{switchingTo === "viewer" ? (
<Loader size="xs" />
) : (
<VisibilityIcon fontSize="small" />
)}
<span>{displayName}</span>
<KeyboardArrowDownIcon fontSize="small" />
</div>
</Menu.Target>
<Menu.Dropdown style={{
backgroundColor: 'var(--right-rail-bg)',
border: '1px solid var(--border-subtle)',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
maxHeight: '50vh',
overflowY: 'auto'
}}>
{activeFiles.map((file, index) => {
const itemName = file?.name || 'Untitled';
const itemDisplayName = itemName.length > 50 ? `${itemName.substring(0, 50)}...` : itemName;
const isActive = index === currentFileIndex;
return (
<Menu.Item
key={file.fileId}
onClick={(e) => {
e.stopPropagation();
onFileSelect?.(index);
}}
className="viewer-file-tab"
{...(isActive && { 'data-active': true })}
style={{
justifyContent: 'flex-start',
}}
>
<Group gap="xs" style={{ width: '100%', justifyContent: 'space-between' }}>
<Text size="sm" style={{ flex: 1, textAlign: 'left' }}>
{itemDisplayName}
</Text>
{file.versionNumber && file.versionNumber > 1 && (
<Text size="xs" c="dimmed">
v{file.versionNumber}
</Text>
)}
</Group>
</Menu.Item>
);
})}
</Menu.Dropdown>
</Menu>
);
};

View File

@ -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 ? (
<FileDropdownMenu
displayName={displayName}
activeFiles={activeFiles}
currentFileIndex={currentFileIndex}
onFileSelect={onFileSelect}
switchingTo={switchingTo}
viewOptionStyle={viewOptionStyle}
/>
) : (
<div style={viewOptionStyle as React.CSSProperties}>
{switchingTo === "viewer" ? (
<Loader size="xs" />
) : (
<VisibilityIcon fontSize="small" />
)}
<span>Viewer</span>
<span>{displayName}</span>
</div>
),
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<WorkbenchType | null>(null);
@ -118,7 +149,7 @@ const TopControls = ({
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
<div className="flex justify-center mt-[0.5rem]">
<SegmentedControl
data={createViewOptions(currentView, switchingTo)}
data={createViewOptions(currentView, switchingTo, activeFiles, currentFileIndex, onFileSelect)}
value={currentView}
onChange={handleViewChange}
color="blue"

View File

@ -1,9 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Box, Center, Text, ActionIcon, Tabs, Collapse, Group, Tooltip } from '@mantine/core';
import { Box, Center, Text, ActionIcon } from '@mantine/core';
import CloseIcon from '@mui/icons-material/Close';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import { useTranslation } from 'react-i18next';
import { useFileState, useFileActions } from "../../contexts/FileContext";
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
@ -22,6 +19,8 @@ export interface EmbedPdfViewerProps {
setSidebarsVisible: (v: boolean) => 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<HTMLDivElement>(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<HTMLDivElement>(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 = ({
</Center>
) : (
<>
{/* Floating tabs for multiple files */}
{activeFiles.length > 1 && !previewFile && (
<Box
ref={tabsContainerRef}
style={{
position: 'absolute',
top: '2rem',
left: 0,
zIndex: 100,
maxWidth: tabsExpanded ? '400px' : 'auto',
transition: 'max-width 0.3s ease',
backgroundColor: 'var(--right-rail-bg)',
borderRight: '1px solid var(--border-subtle)',
borderBottom: '1px solid var(--border-subtle)',
borderRadius: '0 0 8px 0',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
>
<Box
p="xs"
style={{
cursor: 'pointer',
userSelect: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
onClick={() => setTabsExpanded(!tabsExpanded)}
>
<Group gap="xs" style={{ width: '100%' }}>
{tabsExpanded ? <ExpandLessIcon fontSize="small" /> : <ExpandMoreIcon fontSize="small" />}
<Text size="sm" fw={500}>
{t('viewer.files', 'Files')} ({activeFiles.length})
</Text>
</Group>
</Box>
<Collapse in={tabsExpanded}>
<Box style={{ maxHeight: '400px', overflowY: 'auto', overflowX: 'hidden', padding: '0 0.5rem 0.5rem 0.5rem' }}>
<Tabs
value={activeFileIndex.toString()}
onChange={(value) => setActiveFileIndex(parseInt(value || '0'))}
variant="pills"
orientation="vertical"
classNames={{
tab: 'viewer-file-tab'
}}
>
<Tabs.List>
{activeFiles.map((file, index) => {
const stub = selectors.getStirlingFileStub(file.fileId);
const displayName = file.name.length > 25 ? `${file.name.substring(0, 25)}...` : file.name;
return (
<Tooltip
key={file.fileId}
label={file.name}
openDelay={1000}
withArrow
position="right"
>
<Tabs.Tab
value={index.toString()}
>
<Group gap="xs" style={{ width: '100%', justifyContent: 'flex-start' }}>
<Text size="sm" style={{ flex: 1, textAlign: 'left' }}>
{displayName}
</Text>
{stub?.versionNumber && stub.versionNumber > 1 && (
<Text size="xs" c="dimmed">
v{stub.versionNumber}
</Text>
)}
</Group>
</Tabs.Tab>
</Tooltip>
);
})}
</Tabs.List>
</Tabs>
</Box>
</Collapse>
</Box>
)}
{/* EmbedPDF Viewer */}
<Box style={{
position: 'relative',

View File

@ -66,6 +66,10 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
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: {
@ -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,

View File

@ -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,

View File

@ -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) => {