diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index 6efd27047..4475d1ee4 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -44,7 +44,7 @@ const PageEditor = ({ const { setHasUnsavedChanges } = useNavigationGuard(); // Get selected files from PageEditorContext instead of all files - const { selectedFileIds, syncWithFileContext } = usePageEditor(); + const { selectedFileIds, syncWithFileContext, updateCurrentPages, reorderedPages, clearReorderedPages, updateFileOrderFromPages } = usePageEditor(); // Stable reference to file IDs to prevent infinite loops const fileIdsString = state.files.ids.join(','); @@ -137,6 +137,56 @@ const PageEditor = ({ // Interface functions for parent component const displayDocument = editedDocument || mergedPdfDocument; + // Update current pages in context whenever displayDocument changes + useEffect(() => { + if (displayDocument?.pages) { + updateCurrentPages(displayDocument.pages); + } + }, [displayDocument?.pages, updateCurrentPages]); + + // Apply reordered pages from file reordering + useEffect(() => { + if (reorderedPages && reorderedPages.length > 0 && displayDocument) { + // Create a new document with reordered pages + const reorderedDocument: PDFDocument = { + ...displayDocument, + pages: reorderedPages, + totalPages: reorderedPages.length, + }; + setEditedDocument(reorderedDocument); + clearReorderedPages(); + } + }, [reorderedPages, displayDocument, clearReorderedPages]); + + // Update file order when pages are manually reordered + useEffect(() => { + if (editedDocument?.pages && editedDocument.pages.length > 0 && activeFileIds.length > 1) { + // Compute the file order based on page positions + const fileFirstPagePositions = new Map(); + editedDocument.pages.forEach((page, index) => { + const fileId = page.originalFileId; + if (!fileId) return; + if (!fileFirstPagePositions.has(fileId)) { + fileFirstPagePositions.set(fileId, index); + } + }); + + // Sort files by their first page position + const computedFileOrder = Array.from(fileFirstPagePositions.entries()) + .sort((a, b) => a[1] - b[1]) + .map(entry => entry[0]); + + // Check if the order has actually changed + const currentFileOrder = state.files.ids.filter(id => selectedFileIds.has(id)); + const orderChanged = computedFileOrder.length !== currentFileOrder.length || + computedFileOrder.some((id, index) => id !== currentFileOrder[index]); + + if (orderChanged && computedFileOrder.length > 0) { + updateFileOrderFromPages(editedDocument.pages); + } + } + }, [editedDocument?.pages, activeFileIds.length, state.files.ids, selectedFileIds, updateFileOrderFromPages]); + // Utility functions to convert between page IDs and page numbers const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => { if (!displayDocument) return []; @@ -679,6 +729,15 @@ const PageEditor = ({ // Display all pages - use edited or original document const displayedPages = displayDocument?.pages || []; + // Create a mapping of fileId to color index for page highlighting + const fileColorIndexMap = useMemo(() => { + const map = new Map(); + activeFileIds.forEach((fileId, index) => { + map.set(fileId, index); + }); + return map; + }, [activeFileIds]); + return ( @@ -767,34 +826,38 @@ const PageEditor = ({ selectionMode={selectionMode} isAnimating={isAnimating} onReorderPages={handleReorderPages} - renderItem={(page, index, refs) => ( - {}} - onSetMovingPage={setMovingPage} - onDeletePage={handleDeletePage} - createRotateCommand={createRotateCommand} - createDeleteCommand={createDeleteCommand} - createSplitCommand={createSplitCommand} - pdfDocument={displayDocument} - setPdfDocument={setEditedDocument} - splitPositions={splitPositions} - onInsertFiles={handleInsertFiles} - /> - )} + renderItem={(page, index, refs) => { + const fileColorIndex = page.originalFileId ? fileColorIndexMap.get(page.originalFileId) ?? 0 : 0; + return ( + {}} + onSetMovingPage={setMovingPage} + onDeletePage={handleDeletePage} + createRotateCommand={createRotateCommand} + createDeleteCommand={createDeleteCommand} + createSplitCommand={createSplitCommand} + pdfDocument={displayDocument} + setPdfDocument={setEditedDocument} + splitPositions={splitPositions} + onInsertFiles={handleInsertFiles} + /> + ); + }} /> diff --git a/frontend/src/components/pageEditor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx index 59e5819d9..9388a498b 100644 --- a/frontend/src/components/pageEditor/PageThumbnail.tsx +++ b/frontend/src/components/pageEditor/PageThumbnail.tsx @@ -11,6 +11,7 @@ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-d import { PDFPage, PDFDocument } from '../../types/pageEditor'; import { useThumbnailGeneration } from '../../hooks/useThumbnailGeneration'; import { useFilesModalContext } from '../../contexts/FilesModalContext'; +import { getFileColorWithOpacity } from './fileColors'; import styles from './PageEditor.module.css'; @@ -19,6 +20,7 @@ interface PageThumbnailProps { index: number; totalPages: number; originalFile?: File; + fileColorIndex: number; selectedPageIds: string[]; selectionMode: boolean; movingPage: number | null; @@ -45,6 +47,7 @@ const PageThumbnail: React.FC = ({ index, totalPages, originalFile, + fileColorIndex, selectedPageIds, selectionMode, movingPage, @@ -272,6 +275,8 @@ const PageThumbnail: React.FC = ({ setMouseStartPos(null); }, []); + const fileColorBorder = getFileColorWithOpacity(fileColorIndex, 0.5); + return (
= ({ hover:shadow-md transition-all relative - ${selectionMode - ? 'bg-white hover:bg-gray-50' - : 'bg-white hover:bg-gray-50'} + bg-white hover:bg-gray-50 ${isDragging ? 'opacity-50 scale-95' : ''} ${movingPage === page.pageNumber ? 'page-moving' : ''} `} style={{ - transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out' + transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out', }} draggable={false} onMouseDown={handleMouseDown} @@ -346,7 +349,7 @@ const PageThumbnail: React.FC = ({ height: '100%', backgroundColor: 'var(--mantine-color-gray-1)', borderRadius: 6, - border: '1px solid var(--mantine-color-gray-3)', + border: `4px solid ${fileColorBorder}`, padding: 4, display: 'flex', alignItems: 'center', diff --git a/frontend/src/components/pageEditor/fileColors.ts b/frontend/src/components/pageEditor/fileColors.ts new file mode 100644 index 000000000..fa9b0c35f --- /dev/null +++ b/frontend/src/components/pageEditor/fileColors.ts @@ -0,0 +1,61 @@ +/** + * File color palette for page editor + * Each file gets a distinct color for visual organization + * Colors are applied at 0.3 opacity for subtle highlighting + * Maximum 20 files supported in page editor + */ + +export const FILE_COLORS = [ + // Subtle colors (1-6) - fit well with UI theme + 'rgb(59, 130, 246)', // Blue + 'rgb(16, 185, 129)', // Green + 'rgb(139, 92, 246)', // Purple + 'rgb(6, 182, 212)', // Cyan + 'rgb(20, 184, 166)', // Teal + 'rgb(99, 102, 241)', // Indigo + + // Mid-range colors (7-12) - more distinct + 'rgb(244, 114, 182)', // Pink + 'rgb(251, 146, 60)', // Orange + 'rgb(234, 179, 8)', // Yellow + 'rgb(132, 204, 22)', // Lime + 'rgb(248, 113, 113)', // Red + 'rgb(168, 85, 247)', // Violet + + // Vibrant colors (13-20) - maximum distinction + 'rgb(236, 72, 153)', // Fuchsia + 'rgb(245, 158, 11)', // Amber + 'rgb(34, 197, 94)', // Emerald + 'rgb(14, 165, 233)', // Sky + 'rgb(239, 68, 68)', // Rose + 'rgb(168, 162, 158)', // Stone + 'rgb(251, 191, 36)', // Gold + 'rgb(192, 132, 252)', // Light Purple +] as const; + +export const MAX_PAGE_EDITOR_FILES = 20; + +/** + * Get color for a file by its index + * @param index - Zero-based file index + * @returns RGB color string + */ +export function getFileColor(index: number): string { + if (index < 0 || index >= FILE_COLORS.length) { + console.warn(`File index ${index} out of range, using default color`); + return FILE_COLORS[0]; + } + return FILE_COLORS[index]; +} + +/** + * Get color with specified opacity + * @param index - Zero-based file index + * @param opacity - Opacity value (0-1), defaults to 0.3 + * @returns RGBA color string + */ +export function getFileColorWithOpacity(index: number, opacity: number = 0.2): string { + const rgb = getFileColor(index); + // Convert rgb(r, g, b) to rgba(r, g, b, a) + return rgb.replace('rgb(', 'rgba(').replace(')', `, ${opacity})`); +} diff --git a/frontend/src/components/shared/PageEditorFileDropdown.tsx b/frontend/src/components/shared/PageEditorFileDropdown.tsx index eee76e6dc..6a8f59c90 100644 --- a/frontend/src/components/shared/PageEditorFileDropdown.tsx +++ b/frontend/src/components/shared/PageEditorFileDropdown.tsx @@ -1,13 +1,215 @@ -import React from 'react'; +import React, { useRef, useCallback, useState } from 'react'; import { Menu, Loader, Group, Text, Checkbox, ActionIcon } from '@mantine/core'; import EditNoteIcon from '@mui/icons-material/EditNote'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import VerticalAlignTopIcon from '@mui/icons-material/VerticalAlignTop'; +import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom'; +import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import FitText from './FitText'; +import { getFileColorWithOpacity } from '../pageEditor/fileColors'; import { FileId } from '../../types/file'; +interface FileMenuItemProps { + file: { fileId: FileId; name: string; versionNumber?: number }; + index: number; + isSelected: boolean; + isFirst: boolean; + isLast: boolean; + onToggleSelection: (fileId: FileId) => void; + onMoveUp: (e: React.MouseEvent, index: number) => void; + onMoveDown: (e: React.MouseEvent, index: number) => void; + onMoveToTop: (e: React.MouseEvent, index: number) => void; + onMoveToBottom: (e: React.MouseEvent, index: number) => void; + onReorder: (fromIndex: number, toIndex: number) => void; +} + +const FileMenuItem: React.FC = ({ + file, + index, + isSelected, + isFirst, + isLast, + onToggleSelection, + onMoveUp, + onMoveDown, + onMoveToTop, + onMoveToBottom, + onReorder, +}) => { + const [isDragging, setIsDragging] = useState(false); + const [isDragOver, setIsDragOver] = useState(false); + const itemRef = useRef(null); + + const itemElementRef = useCallback((element: HTMLDivElement | null) => { + if (element) { + itemRef.current = element; + + const dragCleanup = draggable({ + element, + getInitialData: () => ({ + type: 'file-item', + fileId: file.fileId, + fromIndex: index, + }), + onDragStart: () => { + setIsDragging(true); + }, + onDrop: () => { + setIsDragging(false); + }, + canDrag: () => true, + }); + + const dropCleanup = dropTargetForElements({ + element, + getData: () => ({ + type: 'file-item', + fileId: file.fileId, + toIndex: index, + }), + onDragEnter: () => { + setIsDragOver(true); + }, + onDragLeave: () => { + setIsDragOver(false); + }, + onDrop: ({ source }) => { + setIsDragOver(false); + const sourceData = source.data; + if (sourceData.type === 'file-item') { + const fromIndex = sourceData.fromIndex as number; + if (fromIndex !== index) { + onReorder(fromIndex, index); + } + } + } + }); + + (element as any).__dragCleanup = () => { + dragCleanup(); + dropCleanup(); + }; + } else { + if (itemRef.current && (itemRef.current as any).__dragCleanup) { + (itemRef.current as any).__dragCleanup(); + } + } + }, [file.fileId, index, onReorder]); + + const itemName = file?.name || 'Untitled'; + const fileColorBorder = getFileColorWithOpacity(index, 1); + const fileColorBorderHover = getFileColorWithOpacity(index, 1.0); + + return ( +
{ + e.stopPropagation(); + onToggleSelection(file.fileId); + }} + style={{ + padding: '0.75rem 0.75rem', + marginBottom: '0.5rem', + cursor: isDragging ? 'grabbing' : 'grab', + backgroundColor: isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent', + borderLeft: `6px solid ${fileColorBorder}`, + borderTop: isDragOver ? '2px solid rgba(0, 0, 0, 0.5)' : 'none', + borderBottom: isDragOver ? '2px solid rgba(0, 0, 0, 0.5)' : 'none', + opacity: isDragging ? 0.5 : 1, + transition: 'opacity 0.2s ease-in-out, background-color 0.15s ease, border-color 0.15s ease', + userSelect: 'none', + }} + onMouseEnter={(e) => { + if (!isDragging) { + (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(0, 0, 0, 0.05)'; + (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorderHover; + } + }} + onMouseLeave={(e) => { + if (!isDragging) { + (e.currentTarget as HTMLDivElement).style.backgroundColor = isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent'; + (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorder; + } + }} + > + +
+
+ +
+ onToggleSelection(file.fileId)} + onClick={(e) => e.stopPropagation()} + size="sm" + /> +
+ +
+ {file.versionNumber && file.versionNumber > 1 && ( + + v{file.versionNumber} + + )} +
+
e.stopPropagation()}> + + + + + + + + + + + + +
+
+
+ ); +}; + interface PageEditorFileDropdownProps { displayName: string; allFiles: Array<{ fileId: FileId; name: string; versionNumber?: number }>; @@ -27,11 +229,6 @@ export const PageEditorFileDropdown: React.FC = ({ switchingTo, viewOptionStyle, }) => { - const handleCheckboxClick = (e: React.MouseEvent, fileId: FileId) => { - e.stopPropagation(); - onToggleSelection(fileId); - }; - const handleMoveUp = (e: React.MouseEvent, index: number) => { e.stopPropagation(); if (index > 0) { @@ -46,8 +243,22 @@ export const PageEditorFileDropdown: React.FC = ({ } }; + const handleMoveToTop = (e: React.MouseEvent, index: number) => { + e.stopPropagation(); + if (index > 0) { + onReorder(index, 0); + } + }; + + const handleMoveToBottom = (e: React.MouseEvent, index: number) => { + e.stopPropagation(); + if (index < allFiles.length - 1) { + onReorder(index, allFiles.length - 1); + } + }; + return ( - +
{switchingTo === "pageEditor" ? ( @@ -64,64 +275,29 @@ export const PageEditorFileDropdown: React.FC = ({ border: '1px solid var(--border-subtle)', borderRadius: '8px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', - maxHeight: '50vh', + maxHeight: '80vh', overflowY: 'auto' }}> {allFiles.map((file, index) => { - const itemName = file?.name || 'Untitled'; const isSelected = selectedFileIds.has(file.fileId); const isFirst = index === 0; const isLast = index === allFiles.length - 1; return ( - e.stopPropagation()} - style={{ - justifyContent: 'flex-start', - cursor: 'default', - backgroundColor: isSelected ? 'var(--bg-hover)' : undefined, - }} - > - -
- {}} - onClick={(e) => handleCheckboxClick(e, file.fileId)} - size="sm" - /> -
- -
- {file.versionNumber && file.versionNumber > 1 && ( - - v{file.versionNumber} - - )} -
-
- handleMoveUp(e, index)} - title="Move up" - > - - - handleMoveDown(e, index)} - title="Move down" - > - - -
-
-
+ file={file} + index={index} + isSelected={isSelected} + isFirst={isFirst} + isLast={isLast} + onToggleSelection={onToggleSelection} + onMoveUp={(e) => handleMoveUp(e, index)} + onMoveDown={(e) => handleMoveDown(e, index)} + onMoveToTop={(e) => handleMoveToTop(e, index)} + onMoveToBottom={(e) => handleMoveToBottom(e, index)} + onReorder={onReorder} + /> ); })} diff --git a/frontend/src/components/shared/TopControls.tsx b/frontend/src/components/shared/TopControls.tsx index c1d78e844..c0de20f59 100644 --- a/frontend/src/components/shared/TopControls.tsx +++ b/frontend/src/components/shared/TopControls.tsx @@ -75,11 +75,7 @@ const createViewOptions = ( let pageEditorDisplayName = 'Page Editor'; if (isInPageEditor && pageEditorState) { - if (pageEditorState.selectedCount === pageEditorState.totalCount) { - pageEditorDisplayName = `${pageEditorState.selectedCount} file${pageEditorState.selectedCount !== 1 ? 's' : ''}`; - } else { - pageEditorDisplayName = `${pageEditorState.selectedCount}/${pageEditorState.totalCount} selected`; - } + pageEditorDisplayName = `${pageEditorState.selectedCount}/${pageEditorState.totalCount} selected`; } const pageEditorOption = { diff --git a/frontend/src/contexts/PageEditorContext.tsx b/frontend/src/contexts/PageEditorContext.tsx index 9486b6b98..fe3ee7e78 100644 --- a/frontend/src/contexts/PageEditorContext.tsx +++ b/frontend/src/contexts/PageEditorContext.tsx @@ -1,11 +1,95 @@ import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'; import { FileId } from '../types/file'; import { useFileActions } from './FileContext'; +import { PDFPage } from '../types/pageEditor'; +import { MAX_PAGE_EDITOR_FILES } from '../components/pageEditor/fileColors'; + +/** + * Computes file order based on the position of each file's first page + * @param pages - Current page order + * @returns Array of FileIds in order based on first page positions + */ +function computeFileOrderFromPages(pages: PDFPage[]): FileId[] { + // Find the first page for each file + const fileFirstPagePositions = new Map(); + + pages.forEach((page, index) => { + const fileId = page.originalFileId; + if (!fileId) return; + + if (!fileFirstPagePositions.has(fileId)) { + fileFirstPagePositions.set(fileId, index); + } + }); + + // Sort files by their first page position + const fileOrder = Array.from(fileFirstPagePositions.entries()) + .sort((a, b) => a[1] - b[1]) + .map(entry => entry[0]); + + return fileOrder; +} + +/** + * Reorders pages based on file reordering while preserving manual page order within files + * @param currentPages - Current page order (may include manual reordering) + * @param fromIndex - Source file index + * @param toIndex - Target file index + * @param orderedFileIds - File IDs in their current order + * @returns Reordered pages with updated page numbers + */ +function reorderPagesForFileMove( + currentPages: PDFPage[], + fromIndex: number, + toIndex: number, + orderedFileIds: FileId[] +): PDFPage[] { + // Group pages by originalFileId, preserving their current relative positions + const fileGroups = new Map(); + + currentPages.forEach(page => { + const fileId = page.originalFileId; + if (!fileId) return; + + if (!fileGroups.has(fileId)) { + fileGroups.set(fileId, []); + } + fileGroups.get(fileId)!.push(page); + }); + + // Reorder the file IDs + const newFileOrder = [...orderedFileIds]; + const [movedFileId] = newFileOrder.splice(fromIndex, 1); + newFileOrder.splice(toIndex, 0, movedFileId); + + // Rebuild pages in new file order, preserving page order within each file + const reorderedPages: PDFPage[] = []; + + newFileOrder.forEach(fileId => { + const filePages = fileGroups.get(fileId) || []; + reorderedPages.push(...filePages); + }); + + // Renumber all pages sequentially + reorderedPages.forEach((page, index) => { + page.pageNumber = index + 1; + }); + + return reorderedPages; +} interface PageEditorContextValue { // Set of selected file IDs (for quick lookup) selectedFileIds: Set; + // Current page order (updated by PageEditor, used for file reordering) + currentPages: PDFPage[] | null; + updateCurrentPages: (pages: PDFPage[] | null) => void; + + // Reordered pages (when file reordering happens) + reorderedPages: PDFPage[] | null; + clearReorderedPages: () => void; + // Toggle file selection toggleFileSelection: (fileId: FileId) => void; @@ -13,9 +97,12 @@ interface PageEditorContextValue { selectAll: (fileIds: FileId[]) => void; deselectAll: () => void; - // Reorder ALL files in FileContext (maintains selection state) + // Reorder ALL files in FileContext (maintains selection state and page order) reorderFiles: (fromIndex: number, toIndex: number, allFileIds: FileId[]) => void; + // Update file order based on page positions (when pages are manually reordered) + updateFileOrderFromPages: (pages: PDFPage[]) => void; + // Sync with FileContext when files change syncWithFileContext: (allFileIds: FileId[]) => void; } @@ -30,14 +117,29 @@ interface PageEditorProviderProps { export function PageEditorProvider({ children, initialFileIds = [] }: PageEditorProviderProps) { // Use Set for O(1) selection lookup const [selectedFileIds, setSelectedFileIds] = useState>(new Set(initialFileIds)); + const [currentPages, setCurrentPages] = useState(null); + const [reorderedPages, setReorderedPages] = useState(null); const { actions: fileActions } = useFileActions(); + const updateCurrentPages = useCallback((pages: PDFPage[] | null) => { + setCurrentPages(pages); + }, []); + + const clearReorderedPages = useCallback(() => { + setReorderedPages(null); + }, []); + const toggleFileSelection = useCallback((fileId: FileId) => { setSelectedFileIds(prev => { const next = new Set(prev); if (next.has(fileId)) { next.delete(fileId); } else { + // Check if adding this file would exceed the limit + if (next.size >= MAX_PAGE_EDITOR_FILES) { + console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Cannot select more files.`); + return prev; + } next.add(fileId); } return next; @@ -45,7 +147,14 @@ export function PageEditorProvider({ children, initialFileIds = [] }: PageEditor }, []); const selectAll = useCallback((fileIds: FileId[]) => { - setSelectedFileIds(new Set(fileIds)); + // Enforce maximum file limit + if (fileIds.length > MAX_PAGE_EDITOR_FILES) { + console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Only first ${MAX_PAGE_EDITOR_FILES} files will be selected.`); + const limitedFiles = fileIds.slice(0, MAX_PAGE_EDITOR_FILES); + setSelectedFileIds(new Set(limitedFiles)); + } else { + setSelectedFileIds(new Set(fileIds)); + } }, []); const deselectAll = useCallback(() => { @@ -60,6 +169,24 @@ export function PageEditorProvider({ children, initialFileIds = [] }: PageEditor // Update global FileContext order fileActions.reorderFiles(newOrder); + + // If current pages available, reorder them based on file move + if (currentPages && currentPages.length > 0) { + const reordered = reorderPagesForFileMove(currentPages, fromIndex, toIndex, allFileIds); + setReorderedPages(reordered); + } + }, [fileActions, currentPages]); + + const updateFileOrderFromPages = useCallback((pages: PDFPage[]) => { + if (!pages || pages.length === 0) return; + + // Compute the new file order based on page positions + const newFileOrder = computeFileOrderFromPages(pages); + + if (newFileOrder.length > 0) { + // Update global FileContext order + fileActions.reorderFiles(newFileOrder); + } }, [fileActions]); const syncWithFileContext = useCallback((allFileIds: FileId[]) => { @@ -72,9 +199,20 @@ export function PageEditorProvider({ children, initialFileIds = [] }: PageEditor } }); - // If no files selected, select all by default + // If no files selected, select all by default (up to MAX_PAGE_EDITOR_FILES) if (next.size === 0 && allFileIds.length > 0) { - return new Set(allFileIds); + const filesToSelect = allFileIds.slice(0, MAX_PAGE_EDITOR_FILES); + if (allFileIds.length > MAX_PAGE_EDITOR_FILES) { + console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Only first ${MAX_PAGE_EDITOR_FILES} files will be selected.`); + } + return new Set(filesToSelect); + } + + // Enforce maximum file limit + if (next.size > MAX_PAGE_EDITOR_FILES) { + console.warn(`Page editor supports maximum ${MAX_PAGE_EDITOR_FILES} files. Limiting selection.`); + const limitedFiles = Array.from(next).slice(0, MAX_PAGE_EDITOR_FILES); + return new Set(limitedFiles); } // Only update if there's an actual change @@ -88,10 +226,15 @@ export function PageEditorProvider({ children, initialFileIds = [] }: PageEditor const value: PageEditorContextValue = { selectedFileIds, + currentPages, + updateCurrentPages, + reorderedPages, + clearReorderedPages, toggleFileSelection, selectAll, deselectAll, reorderFiles, + updateFileOrderFromPages, syncWithFileContext, };