mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-27 07:35:22 +00:00
Page editor redesign
This commit is contained in:
parent
ae508730c3
commit
d981968e0f
@ -68,4 +68,21 @@
|
||||
.languageText {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ripple animation */
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
100% {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Menu, Button, ScrollArea, useMantineTheme, useMantineColorScheme } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supportedLanguages } from '../i18n';
|
||||
@ -10,6 +10,10 @@ const LanguageSelector: React.FC = () => {
|
||||
const theme = useMantineTheme();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [animationTriggered, setAnimationTriggered] = useState(false);
|
||||
const [isChanging, setIsChanging] = useState(false);
|
||||
const [pendingLanguage, setPendingLanguage] = useState<string | null>(null);
|
||||
const [rippleEffect, setRippleEffect] = useState<{x: number, y: number, key: number} | null>(null);
|
||||
|
||||
const languageOptions = Object.entries(supportedLanguages)
|
||||
.sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB))
|
||||
@ -18,22 +22,78 @@ const LanguageSelector: React.FC = () => {
|
||||
label: name,
|
||||
}));
|
||||
|
||||
const handleLanguageChange = (value: string) => {
|
||||
i18n.changeLanguage(value);
|
||||
setOpened(false);
|
||||
const handleLanguageChange = (value: string, event: React.MouseEvent) => {
|
||||
// Create ripple effect at click position
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
setRippleEffect({ x, y, key: Date.now() });
|
||||
|
||||
// Start transition animation
|
||||
setIsChanging(true);
|
||||
setPendingLanguage(value);
|
||||
|
||||
// Simulate processing time for smooth transition
|
||||
setTimeout(() => {
|
||||
i18n.changeLanguage(value);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsChanging(false);
|
||||
setPendingLanguage(null);
|
||||
setOpened(false);
|
||||
|
||||
// Clear ripple effect
|
||||
setTimeout(() => setRippleEffect(null), 100);
|
||||
}, 300);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const currentLanguage = supportedLanguages[i18n.language as keyof typeof supportedLanguages] ||
|
||||
supportedLanguages['en-GB'];
|
||||
|
||||
// Trigger animation when dropdown opens
|
||||
useEffect(() => {
|
||||
if (opened) {
|
||||
setAnimationTriggered(false);
|
||||
// Small delay to ensure DOM is ready
|
||||
setTimeout(() => setAnimationTriggered(true), 50);
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
opened={opened}
|
||||
onChange={setOpened}
|
||||
width={600}
|
||||
position="bottom-start"
|
||||
offset={8}
|
||||
>
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
@keyframes ripple-expand {
|
||||
0% {
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
100% {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<Menu
|
||||
opened={opened}
|
||||
onChange={setOpened}
|
||||
width={600}
|
||||
position="bottom-start"
|
||||
offset={8}
|
||||
transitionProps={{
|
||||
transition: 'scale-y',
|
||||
duration: 200,
|
||||
timingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'
|
||||
}}
|
||||
>
|
||||
<Menu.Target>
|
||||
<Button
|
||||
variant="subtle"
|
||||
@ -43,6 +103,7 @@ const LanguageSelector: React.FC = () => {
|
||||
root: {
|
||||
border: 'none',
|
||||
color: colorScheme === 'dark' ? theme.colors.gray[3] : theme.colors.gray[7],
|
||||
transition: 'background-color 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
'&:hover': {
|
||||
backgroundColor: colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||
}
|
||||
@ -69,22 +130,29 @@ const LanguageSelector: React.FC = () => {
|
||||
>
|
||||
<ScrollArea h={190} type="scroll">
|
||||
<div className={styles.languageGrid}>
|
||||
{languageOptions.map((option) => (
|
||||
{languageOptions.map((option, index) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={styles.languageItem}
|
||||
style={{
|
||||
opacity: animationTriggered ? 1 : 0,
|
||||
transform: animationTriggered ? 'translateY(0px)' : 'translateY(8px)',
|
||||
transition: `opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s, transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.02}s`,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
fullWidth
|
||||
onClick={() => handleLanguageChange(option.value)}
|
||||
onClick={(event) => handleLanguageChange(option.value, event)}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: '4px',
|
||||
minHeight: '32px',
|
||||
padding: '4px 8px',
|
||||
justifyContent: 'flex-start',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: option.value === i18n.language ? (
|
||||
colorScheme === 'dark' ? theme.colors.blue[8] : theme.colors.blue[1]
|
||||
) : 'transparent',
|
||||
@ -93,12 +161,15 @@ const LanguageSelector: React.FC = () => {
|
||||
) : (
|
||||
colorScheme === 'dark' ? theme.colors.gray[3] : theme.colors.gray[7]
|
||||
),
|
||||
transition: 'all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
'&:hover': {
|
||||
backgroundColor: option.value === i18n.language ? (
|
||||
colorScheme === 'dark' ? theme.colors.blue[7] : theme.colors.blue[2]
|
||||
) : (
|
||||
colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1]
|
||||
),
|
||||
transform: 'translateY(-1px)',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
}
|
||||
},
|
||||
label: {
|
||||
@ -108,17 +179,40 @@ const LanguageSelector: React.FC = () => {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
|
||||
{/* Ripple effect */}
|
||||
{rippleEffect && pendingLanguage === option.value && (
|
||||
<div
|
||||
key={rippleEffect.key}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: rippleEffect.x,
|
||||
top: rippleEffect.y,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: theme.colors.blue[4],
|
||||
opacity: 0.6,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
animation: 'ripple-expand 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -22,6 +22,7 @@ import DeselectIcon from "@mui/icons-material/Deselect";
|
||||
import SelectAllIcon from "@mui/icons-material/SelectAll";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { usePDFProcessor } from "../hooks/usePDFProcessor";
|
||||
import { PDFDocument, PDFPage } from "../types/pageEditor";
|
||||
import { fileStorage } from "../services/fileStorage";
|
||||
@ -31,6 +32,7 @@ import {
|
||||
RotatePagesCommand,
|
||||
DeletePagesCommand,
|
||||
ReorderPageCommand,
|
||||
MovePagesCommand,
|
||||
ToggleSplitCommand
|
||||
} from "../commands/pageCommands";
|
||||
import { pdfExportService } from "../services/pdfExportService";
|
||||
@ -57,14 +59,19 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [csvInput, setCsvInput] = useState<string>("");
|
||||
const [showPageSelect, setShowPageSelect] = useState(false);
|
||||
const [selectionMode, setSelectionMode] = useState(false);
|
||||
const [filename, setFilename] = useState<string>("");
|
||||
const [draggedPage, setDraggedPage] = useState<string | null>(null);
|
||||
const [dropTarget, setDropTarget] = useState<string | null>(null);
|
||||
const [multiPageDrag, setMultiPageDrag] = useState<{pageIds: string[], count: number} | null>(null);
|
||||
const [dragPosition, setDragPosition] = useState<{x: number, y: number} | null>(null);
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
const [showExportModal, setShowExportModal] = useState(false);
|
||||
const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null);
|
||||
const [movingPage, setMovingPage] = useState<string | null>(null);
|
||||
const [pagePositions, setPagePositions] = useState<Map<string, { x: number; y: number }>>(new Map());
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const pageRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const fileInputRef = useRef<() => void>(null);
|
||||
|
||||
// Undo/Redo system
|
||||
@ -112,6 +119,32 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
}
|
||||
}, [file, pdfDocument, handleFileUpload]);
|
||||
|
||||
// Global drag cleanup to handle drops outside valid areas
|
||||
useEffect(() => {
|
||||
const handleGlobalDragEnd = () => {
|
||||
// Clean up drag state when drag operation ends anywhere
|
||||
setDraggedPage(null);
|
||||
setDropTarget(null);
|
||||
setMultiPageDrag(null);
|
||||
setDragPosition(null);
|
||||
};
|
||||
|
||||
const handleGlobalDrop = (e: DragEvent) => {
|
||||
// Prevent default to avoid browser navigation on invalid drops
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
if (draggedPage) {
|
||||
document.addEventListener('dragend', handleGlobalDragEnd);
|
||||
document.addEventListener('drop', handleGlobalDrop);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('dragend', handleGlobalDragEnd);
|
||||
document.removeEventListener('drop', handleGlobalDrop);
|
||||
};
|
||||
}, [draggedPage]);
|
||||
|
||||
const selectAll = useCallback(() => {
|
||||
if (pdfDocument) {
|
||||
setSelectedPages(pdfDocument.pages.map(p => p.id));
|
||||
@ -128,6 +161,18 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
);
|
||||
}, []);
|
||||
|
||||
const toggleSelectionMode = useCallback(() => {
|
||||
setSelectionMode(prev => {
|
||||
const newMode = !prev;
|
||||
if (!newMode) {
|
||||
// Clear selections when exiting selection mode
|
||||
setSelectedPages([]);
|
||||
setCsvInput("");
|
||||
}
|
||||
return newMode;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const parseCSVInput = useCallback((csv: string) => {
|
||||
if (!pdfDocument) return [];
|
||||
|
||||
@ -162,12 +207,35 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
|
||||
const handleDragStart = useCallback((pageId: string) => {
|
||||
setDraggedPage(pageId);
|
||||
|
||||
// Check if this is a multi-page drag in selection mode
|
||||
if (selectionMode && selectedPages.includes(pageId) && selectedPages.length > 1) {
|
||||
setMultiPageDrag({
|
||||
pageIds: selectedPages,
|
||||
count: selectedPages.length
|
||||
});
|
||||
} else {
|
||||
setMultiPageDrag(null);
|
||||
}
|
||||
}, [selectionMode, selectedPages]);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
// Clean up drag state regardless of where the drop happened
|
||||
setDraggedPage(null);
|
||||
setDropTarget(null);
|
||||
setMultiPageDrag(null);
|
||||
setDragPosition(null);
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!draggedPage) return;
|
||||
|
||||
// Update drag position for multi-page indicator
|
||||
if (multiPageDrag) {
|
||||
setDragPosition({ x: e.clientX, y: e.clientY });
|
||||
}
|
||||
|
||||
// Get the element under the mouse cursor
|
||||
const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY);
|
||||
@ -192,7 +260,7 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
|
||||
// If not over any valid drop target, clear it
|
||||
setDropTarget(null);
|
||||
}, [draggedPage]);
|
||||
}, [draggedPage, multiPageDrag]);
|
||||
|
||||
const handleDragEnter = useCallback((pageId: string) => {
|
||||
if (draggedPage && pageId !== draggedPage) {
|
||||
@ -204,6 +272,95 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
// Don't clear drop target on drag leave - let dragover handle it
|
||||
}, []);
|
||||
|
||||
const animateReorder = useCallback((pageId: string, targetIndex: number) => {
|
||||
if (!pdfDocument || isAnimating) return;
|
||||
|
||||
// In selection mode, if the dragged page is selected, move all selected pages
|
||||
const pagesToMove = selectionMode && selectedPages.includes(pageId)
|
||||
? selectedPages
|
||||
: [pageId];
|
||||
|
||||
const originalIndex = pdfDocument.pages.findIndex(p => p.id === pageId);
|
||||
if (originalIndex === -1 || originalIndex === targetIndex) return;
|
||||
|
||||
setIsAnimating(true);
|
||||
|
||||
// Get current positions of all pages
|
||||
const currentPositions = new Map<string, { x: number; y: number }>();
|
||||
pdfDocument.pages.forEach((page) => {
|
||||
const element = pageRefs.current.get(page.id);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
currentPositions.set(page.id, { x: rect.left, y: rect.top });
|
||||
}
|
||||
});
|
||||
|
||||
// Execute the reorder - for multi-page, we use a different command
|
||||
if (pagesToMove.length > 1) {
|
||||
// Multi-page move - use MovePagesCommand
|
||||
const command = new MovePagesCommand(pdfDocument, setPdfDocument, pagesToMove, targetIndex);
|
||||
executeCommand(command);
|
||||
} else {
|
||||
// Single page move
|
||||
const command = new ReorderPageCommand(pdfDocument, setPdfDocument, pageId, targetIndex);
|
||||
executeCommand(command);
|
||||
}
|
||||
|
||||
// Wait for DOM to update, then get new positions and animate
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const newPositions = new Map<string, { x: number; y: number }>();
|
||||
|
||||
// Get the updated document from the state after command execution
|
||||
// The command has already updated the document, so we need to get the new order
|
||||
const currentDoc = pdfDocument; // This should be the updated version after command
|
||||
|
||||
currentDoc.pages.forEach((page) => {
|
||||
const element = pageRefs.current.get(page.id);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
newPositions.set(page.id, { x: rect.left, y: rect.top });
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate and apply animations
|
||||
currentDoc.pages.forEach((page) => {
|
||||
const element = pageRefs.current.get(page.id);
|
||||
const currentPos = currentPositions.get(page.id);
|
||||
const newPos = newPositions.get(page.id);
|
||||
|
||||
if (element && currentPos && newPos) {
|
||||
const deltaX = currentPos.x - newPos.x;
|
||||
const deltaY = currentPos.y - newPos.y;
|
||||
|
||||
// Apply initial transform (from new position back to old position)
|
||||
element.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
||||
element.style.transition = 'none';
|
||||
|
||||
// Force reflow
|
||||
element.offsetHeight;
|
||||
|
||||
// Animate to final position
|
||||
element.style.transition = 'transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
||||
element.style.transform = 'translate(0px, 0px)';
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up after animation
|
||||
setTimeout(() => {
|
||||
currentDoc.pages.forEach((page) => {
|
||||
const element = pageRefs.current.get(page.id);
|
||||
if (element) {
|
||||
element.style.transform = '';
|
||||
element.style.transition = '';
|
||||
}
|
||||
});
|
||||
setIsAnimating(false);
|
||||
}, 400);
|
||||
});
|
||||
});
|
||||
}, [pdfDocument, isAnimating, executeCommand, selectionMode, selectedPages]);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent, targetPageId: string | 'end') => {
|
||||
e.preventDefault();
|
||||
if (!draggedPage || !pdfDocument || draggedPage === targetPageId) return;
|
||||
@ -216,18 +373,16 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
if (targetIndex === -1) return;
|
||||
}
|
||||
|
||||
const command = new ReorderPageCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
draggedPage,
|
||||
targetIndex
|
||||
);
|
||||
|
||||
executeCommand(command);
|
||||
animateReorder(draggedPage, targetIndex);
|
||||
|
||||
setDraggedPage(null);
|
||||
setDropTarget(null);
|
||||
setStatus('Page reordered');
|
||||
}, [draggedPage, pdfDocument, executeCommand]);
|
||||
setMultiPageDrag(null);
|
||||
setDragPosition(null);
|
||||
|
||||
const moveCount = multiPageDrag ? multiPageDrag.count : 1;
|
||||
setStatus(`${moveCount > 1 ? `${moveCount} pages` : 'Page'} reordered`);
|
||||
}, [draggedPage, pdfDocument, animateReorder, multiPageDrag]);
|
||||
|
||||
const handleEndZoneDragEnter = useCallback(() => {
|
||||
if (draggedPage) {
|
||||
@ -236,46 +391,69 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
}, [draggedPage]);
|
||||
|
||||
const handleRotate = useCallback((direction: 'left' | 'right') => {
|
||||
if (!pdfDocument || selectedPages.length === 0) return;
|
||||
if (!pdfDocument) return;
|
||||
|
||||
const rotation = direction === 'left' ? -90 : 90;
|
||||
const pagesToRotate = selectionMode
|
||||
? selectedPages
|
||||
: pdfDocument.pages.map(p => p.id);
|
||||
|
||||
if (selectionMode && selectedPages.length === 0) return;
|
||||
|
||||
const command = new RotatePagesCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
selectedPages,
|
||||
pagesToRotate,
|
||||
rotation
|
||||
);
|
||||
|
||||
executeCommand(command);
|
||||
setStatus(`Rotated ${selectedPages.length} pages ${direction}`);
|
||||
}, [pdfDocument, selectedPages, executeCommand]);
|
||||
const pageCount = selectionMode ? selectedPages.length : pdfDocument.pages.length;
|
||||
setStatus(`Rotated ${pageCount} pages ${direction}`);
|
||||
}, [pdfDocument, selectedPages, selectionMode, executeCommand]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!pdfDocument || selectedPages.length === 0) return;
|
||||
if (!pdfDocument) return;
|
||||
|
||||
const pagesToDelete = selectionMode
|
||||
? selectedPages
|
||||
: pdfDocument.pages.map(p => p.id);
|
||||
|
||||
if (selectionMode && selectedPages.length === 0) return;
|
||||
|
||||
const command = new DeletePagesCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
selectedPages
|
||||
pagesToDelete
|
||||
);
|
||||
|
||||
executeCommand(command);
|
||||
setSelectedPages([]);
|
||||
setStatus(`Deleted ${selectedPages.length} pages`);
|
||||
}, [pdfDocument, selectedPages, executeCommand]);
|
||||
if (selectionMode) {
|
||||
setSelectedPages([]);
|
||||
}
|
||||
const pageCount = selectionMode ? selectedPages.length : pdfDocument.pages.length;
|
||||
setStatus(`Deleted ${pageCount} pages`);
|
||||
}, [pdfDocument, selectedPages, selectionMode, executeCommand]);
|
||||
|
||||
const handleSplit = useCallback(() => {
|
||||
if (!pdfDocument || selectedPages.length === 0) return;
|
||||
if (!pdfDocument) return;
|
||||
|
||||
const pagesToSplit = selectionMode
|
||||
? selectedPages
|
||||
: pdfDocument.pages.map(p => p.id);
|
||||
|
||||
if (selectionMode && selectedPages.length === 0) return;
|
||||
|
||||
const command = new ToggleSplitCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
selectedPages
|
||||
pagesToSplit
|
||||
);
|
||||
|
||||
executeCommand(command);
|
||||
setStatus(`Split markers toggled for ${selectedPages.length} pages`);
|
||||
}, [pdfDocument, selectedPages, executeCommand]);
|
||||
const pageCount = selectionMode ? selectedPages.length : pdfDocument.pages.length;
|
||||
setStatus(`Split markers toggled for ${pageCount} pages`);
|
||||
}, [pdfDocument, selectedPages, selectionMode, executeCommand]);
|
||||
|
||||
const showExportPreview = useCallback((selectedOnly: boolean = false) => {
|
||||
if (!pdfDocument) return;
|
||||
@ -390,6 +568,10 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
.page-container:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.checkbox-container {
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
.page-move-animation {
|
||||
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
@ -398,6 +580,22 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.multi-drag-indicator {
|
||||
position: fixed;
|
||||
background: rgba(59, 130, 246, 0.9);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
@ -410,7 +608,7 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
</style>
|
||||
<LoadingOverlay visible={loading || pdfLoading} />
|
||||
|
||||
<Box p="md">
|
||||
<Box p="md" pt="xl">
|
||||
<Group mb="md">
|
||||
<TextInput
|
||||
value={filename}
|
||||
@ -418,76 +616,60 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
placeholder="Enter filename"
|
||||
style={{ minWidth: 200 }}
|
||||
/>
|
||||
<Button onClick={() => setShowPageSelect(!showPageSelect)}>
|
||||
Select Pages
|
||||
<Button
|
||||
onClick={toggleSelectionMode}
|
||||
variant={selectionMode ? "filled" : "outline"}
|
||||
color={selectionMode ? "blue" : "gray"}
|
||||
styles={{
|
||||
root: {
|
||||
transition: 'all 0.2s ease',
|
||||
...(selectionMode && {
|
||||
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.3)',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectionMode ? "Exit Selection" : "Select Pages"}
|
||||
</Button>
|
||||
<Button onClick={selectAll}>Select All</Button>
|
||||
<Button onClick={deselectAll}>Deselect All</Button>
|
||||
{selectionMode && (
|
||||
<>
|
||||
<Button onClick={selectAll} variant="light">Select All</Button>
|
||||
<Button onClick={deselectAll} variant="light">Deselect All</Button>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{showPageSelect && (
|
||||
<Paper p="md" mb="md" withBorder>
|
||||
<Group>
|
||||
<TextInput
|
||||
value={csvInput}
|
||||
onChange={(e) => setCsvInput(e.target.value)}
|
||||
placeholder="1,3,5-10"
|
||||
label="Page Selection"
|
||||
onBlur={updatePagesFromCSV}
|
||||
onKeyDown={(e) => e.key === 'Enter' && updatePagesFromCSV()}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button onClick={updatePagesFromCSV} mt="xl">
|
||||
Apply
|
||||
</Button>
|
||||
</Group>
|
||||
{selectedPages.length > 0 && (
|
||||
<Text size="sm" c="dimmed" mt="sm">
|
||||
Selected: {selectedPages.length} pages
|
||||
</Text>
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Group mb="md">
|
||||
<Tooltip label="Undo">
|
||||
<ActionIcon onClick={handleUndo} disabled={!canUndo}>
|
||||
<UndoIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Redo">
|
||||
<ActionIcon onClick={handleRedo} disabled={!canRedo}>
|
||||
<RedoIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Rotate Left">
|
||||
<ActionIcon onClick={() => handleRotate('left')} disabled={selectedPages.length === 0}>
|
||||
<RotateLeftIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Rotate Right">
|
||||
<ActionIcon onClick={() => handleRotate('right')} disabled={selectedPages.length === 0}>
|
||||
<RotateRightIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete">
|
||||
<ActionIcon onClick={handleDelete} disabled={selectedPages.length === 0} color="red">
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Split">
|
||||
<ActionIcon onClick={handleSplit} disabled={selectedPages.length === 0}>
|
||||
<ContentCutIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
{selectionMode && (
|
||||
<Paper p="md" mb="md" withBorder>
|
||||
<Group>
|
||||
<TextInput
|
||||
value={csvInput}
|
||||
onChange={(e) => setCsvInput(e.target.value)}
|
||||
placeholder="1,3,5-10"
|
||||
label="Page Selection"
|
||||
onBlur={updatePagesFromCSV}
|
||||
onKeyDown={(e) => e.key === 'Enter' && updatePagesFromCSV()}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button onClick={updatePagesFromCSV} mt="xl">
|
||||
Apply
|
||||
</Button>
|
||||
</Group>
|
||||
{selectedPages.length > 0 && (
|
||||
<Text size="sm" c="dimmed" mt="sm">
|
||||
Selected: {selectedPages.length} pages
|
||||
</Text>
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '1.5rem',
|
||||
justifyContent: 'flex-start'
|
||||
justifyContent: 'flex-start',
|
||||
paddingBottom: '100px' // Add space for floating control bar
|
||||
}}
|
||||
>
|
||||
{pdfDocument.pages.map((page, index) => (
|
||||
@ -506,6 +688,13 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
pageRefs.current.set(page.id, el);
|
||||
} else {
|
||||
pageRefs.current.delete(page.id);
|
||||
}
|
||||
}}
|
||||
data-page-id={page.id}
|
||||
className={`
|
||||
!rounded-lg
|
||||
@ -519,31 +708,67 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
hover:shadow-md
|
||||
transition-all
|
||||
relative
|
||||
page-move-animation
|
||||
${selectedPages.includes(page.id)
|
||||
? 'ring-2 ring-blue-500 bg-blue-50'
|
||||
${selectionMode
|
||||
? 'bg-white hover:bg-gray-50'
|
||||
: 'bg-white hover:bg-gray-50'}
|
||||
${draggedPage === page.id ? 'opacity-50 scale-95' : ''}
|
||||
${movingPage === page.id ? 'page-moving' : ''}
|
||||
`}
|
||||
style={{
|
||||
transform: (() => {
|
||||
if (!draggedPage || page.id === draggedPage) return 'translateX(0)';
|
||||
|
||||
if (dropTarget === page.id) {
|
||||
return 'translateX(20px)'; // Move slightly right to indicate drop position
|
||||
}
|
||||
return 'translateX(0)';
|
||||
})(),
|
||||
transition: 'transform 0.2s ease-in-out'
|
||||
}}
|
||||
style={{
|
||||
transform: (() => {
|
||||
// Only apply drop target indication during drag
|
||||
if (!isAnimating && draggedPage && page.id !== draggedPage && dropTarget === page.id) {
|
||||
return 'translateX(20px)';
|
||||
}
|
||||
return 'translateX(0)';
|
||||
})(),
|
||||
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out'
|
||||
}}
|
||||
draggable
|
||||
onDragStart={() => handleDragStart(page.id)}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={() => handleDragEnter(page.id)}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={(e) => handleDrop(e, page.id)}
|
||||
>
|
||||
{/* Selection mode checkbox - positioned outside page-container to avoid transform inheritance */}
|
||||
{selectionMode && (
|
||||
<div
|
||||
className="checkbox-container"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 4,
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '4px',
|
||||
padding: '2px',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
pointerEvents: 'auto' // Ensure checkbox can be clicked
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation(); // Prevent drag from starting
|
||||
}}
|
||||
onDragStart={(e) => {
|
||||
e.preventDefault(); // Prevent drag on checkbox
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedPages.includes(page.id)}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation();
|
||||
togglePage(page.id);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="page-container w-[90%] h-[90%]">
|
||||
<img
|
||||
src={page.thumbnail}
|
||||
@ -607,19 +832,11 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
disabled={index === 0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (index > 0 && !movingPage) {
|
||||
if (index > 0 && !movingPage && !isAnimating) {
|
||||
setMovingPage(page.id);
|
||||
setTimeout(() => {
|
||||
const command = new ReorderPageCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
page.id,
|
||||
index - 1
|
||||
);
|
||||
executeCommand(command);
|
||||
setTimeout(() => setMovingPage(null), 100);
|
||||
setStatus(`Moved page ${page.pageNumber} left`);
|
||||
}, 50);
|
||||
animateReorder(page.id, index - 1);
|
||||
setTimeout(() => setMovingPage(null), 500);
|
||||
setStatus(`Moved page ${page.pageNumber} left`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -635,19 +852,11 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
disabled={index === pdfDocument.pages.length - 1}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (index < pdfDocument.pages.length - 1 && !movingPage) {
|
||||
if (index < pdfDocument.pages.length - 1 && !movingPage && !isAnimating) {
|
||||
setMovingPage(page.id);
|
||||
setTimeout(() => {
|
||||
const command = new ReorderPageCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
page.id,
|
||||
index + 1
|
||||
);
|
||||
executeCommand(command);
|
||||
setTimeout(() => setMovingPage(null), 100);
|
||||
setStatus(`Moved page ${page.pageNumber} right`);
|
||||
}, 50);
|
||||
animateReorder(page.id, index + 1);
|
||||
setTimeout(() => setMovingPage(null), 500);
|
||||
setStatus(`Moved page ${page.pageNumber} right`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -739,16 +948,6 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip label="Select Page">
|
||||
<Checkbox
|
||||
size="md"
|
||||
checked={selectedPages.includes(page.id)}
|
||||
onChange={() => togglePage(page.id)}
|
||||
styles={{
|
||||
input: { backgroundColor: 'white' }
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<DragIndicatorIcon
|
||||
@ -786,38 +985,142 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Group justify="space-between" mt="md">
|
||||
<Button
|
||||
color="red"
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setPdfDocument(null);
|
||||
setFile && setFile(null);
|
||||
}}
|
||||
>
|
||||
Close PDF
|
||||
</Button>
|
||||
|
||||
<Group>
|
||||
<Button
|
||||
leftSection={<DownloadIcon />}
|
||||
disabled={selectedPages.length === 0 || exportLoading}
|
||||
loading={exportLoading}
|
||||
onClick={() => showExportPreview(true)}
|
||||
{/* Floating control bar */}
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: '50%',
|
||||
bottom: '20px',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 50,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
radius="xl"
|
||||
shadow="lg"
|
||||
p={16}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
borderRadius: 32,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
|
||||
pointerEvents: 'auto',
|
||||
minWidth: 400,
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{/* Close PDF */}
|
||||
<Tooltip label="Close PDF">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
setPdfDocument(null);
|
||||
setFile && setFile(null);
|
||||
}}
|
||||
color="red"
|
||||
variant="light"
|
||||
size="lg"
|
||||
>
|
||||
Download Selected
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
disabled={exportLoading}
|
||||
loading={exportLoading}
|
||||
<CloseIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<div style={{ width: 1, height: 28, backgroundColor: 'var(--mantine-color-gray-3)', margin: '0 8px' }} />
|
||||
|
||||
{/* Undo/Redo */}
|
||||
<Tooltip label="Undo">
|
||||
<ActionIcon onClick={handleUndo} disabled={!canUndo} size="lg">
|
||||
<UndoIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Redo">
|
||||
<ActionIcon onClick={handleRedo} disabled={!canRedo} size="lg">
|
||||
<RedoIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<div style={{ width: 1, height: 28, backgroundColor: 'var(--mantine-color-gray-3)', margin: '0 8px' }} />
|
||||
|
||||
{/* Page Operations */}
|
||||
<Tooltip label={selectionMode ? "Rotate Selected Left" : "Rotate All Left"}>
|
||||
<ActionIcon
|
||||
onClick={() => handleRotate('left')}
|
||||
disabled={selectionMode && selectedPages.length === 0}
|
||||
variant={selectionMode && selectedPages.length > 0 ? "light" : "default"}
|
||||
color={selectionMode && selectedPages.length > 0 ? "blue" : undefined}
|
||||
size="lg"
|
||||
>
|
||||
<RotateLeftIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={selectionMode ? "Rotate Selected Right" : "Rotate All Right"}>
|
||||
<ActionIcon
|
||||
onClick={() => handleRotate('right')}
|
||||
disabled={selectionMode && selectedPages.length === 0}
|
||||
variant={selectionMode && selectedPages.length > 0 ? "light" : "default"}
|
||||
color={selectionMode && selectedPages.length > 0 ? "blue" : undefined}
|
||||
size="lg"
|
||||
>
|
||||
<RotateRightIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={selectionMode ? "Delete Selected" : "Delete All"}>
|
||||
<ActionIcon
|
||||
onClick={handleDelete}
|
||||
disabled={selectionMode && selectedPages.length === 0}
|
||||
color="red"
|
||||
variant={selectionMode && selectedPages.length > 0 ? "light" : "default"}
|
||||
size="lg"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={selectionMode ? "Split Selected" : "Split All"}>
|
||||
<ActionIcon
|
||||
onClick={handleSplit}
|
||||
disabled={selectionMode && selectedPages.length === 0}
|
||||
variant={selectionMode && selectedPages.length > 0 ? "light" : "default"}
|
||||
color={selectionMode && selectedPages.length > 0 ? "blue" : undefined}
|
||||
size="lg"
|
||||
>
|
||||
<ContentCutIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<div style={{ width: 1, height: 28, backgroundColor: 'var(--mantine-color-gray-3)', margin: '0 8px' }} />
|
||||
|
||||
{/* Export Controls */}
|
||||
{selectionMode && selectedPages.length > 0 && (
|
||||
<Tooltip label="Export Selected">
|
||||
<ActionIcon
|
||||
onClick={() => showExportPreview(true)}
|
||||
disabled={exportLoading}
|
||||
color="blue"
|
||||
variant="light"
|
||||
size="lg"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label="Export All">
|
||||
<ActionIcon
|
||||
onClick={() => showExportPreview(false)}
|
||||
disabled={exportLoading}
|
||||
color="green"
|
||||
variant="light"
|
||||
size="lg"
|
||||
>
|
||||
Download All
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
<DownloadIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Paper>
|
||||
</div>
|
||||
|
||||
</Box>
|
||||
|
||||
<Modal
|
||||
@ -890,6 +1193,19 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
{status}
|
||||
</Notification>
|
||||
)}
|
||||
|
||||
{/* Multi-page drag indicator */}
|
||||
{multiPageDrag && dragPosition && (
|
||||
<div
|
||||
className="multi-drag-indicator"
|
||||
style={{
|
||||
left: dragPosition.x,
|
||||
top: dragPosition.y,
|
||||
}}
|
||||
>
|
||||
{multiPageDrag.count} pages
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -534,23 +534,6 @@ const Viewer: React.FC<ViewerProps> = ({
|
||||
>
|
||||
{dualPage ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="md"
|
||||
radius="xl"
|
||||
onClick={() => setSidebarsVisible(!sidebarsVisible)}
|
||||
style={{ minWidth: 36 }}
|
||||
title={sidebarsVisible ? t("viewer.hideSidebars", "Hide Sidebars") : t("viewer.showSidebars", "Show Sidebars")}
|
||||
>
|
||||
<ViewSidebarIcon
|
||||
fontSize="small"
|
||||
style={{
|
||||
transform: sidebarsVisible ? "none" : "scaleX(-1)",
|
||||
transition: "transform 0.2s"
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<Group gap={4} align="center" style={{ marginLeft: 16 }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
|
@ -107,66 +107,80 @@ export default function HomePage() {
|
||||
/>
|
||||
|
||||
{/* Left: Tool Picker OR Selected Tool Panel */}
|
||||
{sidebarsVisible && !readerMode && (
|
||||
<div
|
||||
className={`h-screen z-sticky flex flex-col min-w-[300px] max-w-[450px] w-[25vw] ${isRainbowMode ? rainbowStyles.rainbowPaper : ''}`}
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-surface)',
|
||||
borderRight: '1px solid var(--border-subtle)',
|
||||
padding: '1rem'
|
||||
}}
|
||||
>
|
||||
{leftPanelView === 'toolPicker' ? (
|
||||
// Tool Picker View
|
||||
<div className="flex-1 flex flex-col">
|
||||
<ToolPicker
|
||||
selectedToolKey={selectedToolKey}
|
||||
onSelect={handleToolSelect}
|
||||
toolRegistry={toolRegistry}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Selected Tool Content View
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Back button */}
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={() => setLeftPanelView('toolPicker')}
|
||||
className="text-sm"
|
||||
>
|
||||
← Back to Tools
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tool title */}
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-semibold">{selectedTool?.name}</h2>
|
||||
</div>
|
||||
|
||||
{/* Tool content */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<ToolRenderer
|
||||
<div
|
||||
className={`h-screen z-sticky flex flex-col ${isRainbowMode ? rainbowStyles.rainbowPaper : ''} overflow-hidden`}
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-surface)',
|
||||
borderRight: '1px solid var(--border-subtle)',
|
||||
width: sidebarsVisible && !readerMode ? '25vw' : '0px',
|
||||
minWidth: sidebarsVisible && !readerMode ? '300px' : '0px',
|
||||
maxWidth: sidebarsVisible && !readerMode ? '450px' : '0px',
|
||||
transition: 'width 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), min-width 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), max-width 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
padding: sidebarsVisible && !readerMode ? '1rem' : '0rem'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
opacity: sidebarsVisible && !readerMode ? 1 : 0,
|
||||
transition: 'opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
{leftPanelView === 'toolPicker' ? (
|
||||
// Tool Picker View
|
||||
<div className="flex-1 flex flex-col">
|
||||
<ToolPicker
|
||||
selectedToolKey={selectedToolKey}
|
||||
selectedTool={selectedTool}
|
||||
pdfFile={pdfFile}
|
||||
files={files}
|
||||
downloadUrl={downloadUrl}
|
||||
setDownloadUrl={setDownloadUrl}
|
||||
toolParams={toolParams}
|
||||
updateParams={updateParams}
|
||||
onSelect={handleToolSelect}
|
||||
toolRegistry={toolRegistry}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
// Selected Tool Content View
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Back button */}
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={() => setLeftPanelView('toolPicker')}
|
||||
className="text-sm"
|
||||
>
|
||||
← Back to Tools
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tool title */}
|
||||
<div className="mb-4">
|
||||
<h2 className="text-lg font-semibold">{selectedTool?.name}</h2>
|
||||
</div>
|
||||
|
||||
{/* Tool content */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<ToolRenderer
|
||||
selectedToolKey={selectedToolKey}
|
||||
selectedTool={selectedTool}
|
||||
pdfFile={pdfFile}
|
||||
files={files}
|
||||
downloadUrl={downloadUrl}
|
||||
setDownloadUrl={setDownloadUrl}
|
||||
toolParams={toolParams}
|
||||
updateParams={updateParams}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main View */}
|
||||
<Box
|
||||
className="flex-1 h-screen min-w-80 relative flex flex-col transition-all duration-300"
|
||||
style={{ backgroundColor: 'var(--bg-background)' }}
|
||||
className="flex-1 h-screen min-w-80 relative flex flex-col"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-background)'
|
||||
}}
|
||||
>
|
||||
{/* Top Controls */}
|
||||
<TopControls
|
||||
|
Loading…
x
Reference in New Issue
Block a user