From cf9a0dd0b7cb4d6a2dbadce233f301575675948c Mon Sep 17 00:00:00 2001 From: zemion Date: Sun, 17 May 2026 02:39:32 +0200 Subject: [PATCH] mostly formatting, dependency fix --- prettierignore.txt => .prettierignore | 0 prettierrc.json => .prettierrc.json | 0 eslint.config.mjs | 42 +-- src/App.tsx | 281 +++++++++--------- src/components/ActionDialog.tsx | 118 ++++---- src/components/ActionsPanel.tsx | 28 +- src/components/FileLoader.tsx | 8 +- src/components/HelpDialog.tsx | 58 ++-- src/components/Layout.tsx | 4 +- src/components/PagePreviewModal.tsx | 157 +++++----- .../PageWorkspace/CopyPagesDialog.tsx | 148 ++++----- .../PageWorkspace/DropIndicator.tsx | 20 +- src/components/PageWorkspace/PageCard.tsx | 126 ++++---- src/components/PageWorkspace/PageGrid.tsx | 30 +- .../PageWorkspace/PageSelectionToolbar.tsx | 52 ++-- src/components/ReorderPanel.tsx | 56 ++-- src/components/WorkspacePanel.tsx | 156 +++++----- src/hooks/usePdfGeneratedOutputs.ts | 14 +- src/main.tsx | 12 +- src/pdf/pdfService.ts | 38 +-- src/pdf/pdfThumbnailService.ts | 39 +-- src/pdf/pdfTypes.ts | 2 +- src/pdf/usePdfThumbnails.ts | 30 +- src/styles.css | 2 +- src/version.ts | 2 +- src/workspace/useWorkspaceState.test.tsx | 82 ++--- src/workspace/useWorkspaceState.ts | 36 +-- src/workspace/workspaceCommands.test.ts | 72 ++--- src/workspace/workspaceCommands.ts | 8 +- src/workspace/workspaceDb.ts | 42 +-- src/workspace/workspaceTypes.ts | 4 +- vite.config.ts | 6 +- 32 files changed, 837 insertions(+), 836 deletions(-) rename prettierignore.txt => .prettierignore (100%) rename prettierrc.json => .prettierrc.json (100%) diff --git a/prettierignore.txt b/.prettierignore similarity index 100% rename from prettierignore.txt rename to .prettierignore diff --git a/prettierrc.json b/.prettierrc.json similarity index 100% rename from prettierrc.json rename to .prettierrc.json diff --git a/eslint.config.mjs b/eslint.config.mjs index 5388e5a..dff2c36 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,49 +1,49 @@ -import js from "@eslint/js"; -import eslintConfigPrettier from "eslint-config-prettier"; -import reactHooks from "eslint-plugin-react-hooks"; -import reactRefresh from "eslint-plugin-react-refresh"; -import globals from "globals"; -import tseslint from "typescript-eslint"; +import js from '@eslint/js'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; export default tseslint.config( { - ignores: ["dist", "coverage", "node_modules"], + ignores: ['dist', 'coverage', 'node_modules'], }, js.configs.recommended, ...tseslint.configs.recommended, { - files: ["**/*.{ts,tsx}"], + files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2022, - sourceType: "module", + sourceType: 'module', globals: { ...globals.browser, ...globals.es2022, }, }, plugins: { - "react-hooks": reactHooks, - "react-refresh": reactRefresh, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": [ - "warn", + 'react-refresh/only-export-components': [ + 'warn', { allowConstantExport: true }, ], - "react-hooks/set-state-in-effect": "off", - "@typescript-eslint/no-unused-vars": [ - "warn", + 'react-hooks/set-state-in-effect': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', }, ], }, }, { - files: ["*.config.{js,ts}", "eslint.config.js"], + files: ['*.config.{js,ts}', 'eslint.config.js'], languageOptions: { globals: { ...globals.node, @@ -51,5 +51,5 @@ export default tseslint.config( }, }, }, - eslintConfigPrettier, + eslintConfigPrettier ); diff --git a/src/App.tsx b/src/App.tsx index 63d2d65..f46a26a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,19 @@ -import React, { useCallback, useEffect, useState } from "react"; -import Layout from "./components/Layout"; -import FileLoader from "./components/FileLoader"; -import ReorderPanel from "./components/ReorderPanel"; -import ActionsPanel from "./components/ActionsPanel"; -import PagePreviewModal from "./components/PagePreviewModal"; -import WorkspacePanel from "./components/WorkspacePanel"; +import React, { useCallback, useEffect, useState } from 'react'; +import Layout from './components/Layout'; +import FileLoader from './components/FileLoader'; +import ReorderPanel from './components/ReorderPanel'; +import ActionsPanel from './components/ActionsPanel'; +import PagePreviewModal from './components/PagePreviewModal'; +import WorkspacePanel from './components/WorkspacePanel'; import ActionDialog, { type ActionDialogAction, -} from "./components/ActionDialog"; -import HelpDialog from "./components/HelpDialog"; -import { PDFDocument } from "pdf-lib"; +} from './components/ActionDialog'; +import HelpDialog from './components/HelpDialog'; +import { PDFDocument } from 'pdf-lib'; import type { StoredWorkspace, WorkspaceSummary, -} from "./workspace/workspaceTypes"; +} from './workspace/workspaceTypes'; import { createInitialPageRefs, createPageRefId, @@ -22,22 +22,22 @@ import { defaultWorkspaceNameFromPdfName, normalizeRotation, useWorkspaceState, -} from "./workspace/useWorkspaceState"; +} from './workspace/useWorkspaceState'; import { deleteWorkspaceFromIndexedDb, listWorkspaces, loadWorkspaceFromIndexedDb, saveWorkspaceToIndexedDb, -} from "./workspace/workspaceDb"; -import type { PageRef, PdfFile } from "./pdf/pdfTypes"; +} from './workspace/workspaceDb'; +import type { PageRef, PdfFile } from './pdf/pdfTypes'; import { loadPdfFromFile, mergePdfFiles, splitIntoSinglePages, exportPages, -} from "./pdf/pdfService"; -import { usePdfThumbnails } from "./pdf/usePdfThumbnails"; -import { usePdfGeneratedOutputs } from "./hooks/usePdfGeneratedOutputs"; +} from './pdf/pdfService'; +import { usePdfThumbnails } from './pdf/usePdfThumbnails'; +import { usePdfGeneratedOutputs } from './hooks/usePdfGeneratedOutputs'; import { createSelectionPdfName, createSelectionWorkspaceName, @@ -50,9 +50,9 @@ function isEditableKeyboardTarget(target: EventTarget | null): boolean { const tagName = target.tagName.toLowerCase(); return ( target.isContentEditable || - tagName === "input" || - tagName === "textarea" || - tagName === "select" + tagName === 'input' || + tagName === 'textarea' || + tagName === 'select' ); } @@ -70,18 +70,18 @@ const App: React.FC = () => { const [workspaces, setWorkspaces] = useState([]); const [activeWorkspaceId, setActiveWorkspaceId] = useState( - null, + null ); - const [workspaceName, setWorkspaceName] = useState(""); + const [workspaceName, setWorkspaceName] = useState(''); const [previewPageId, setPreviewPageId] = useState(null); const [pendingFile, setPendingFile] = useState(null); const [showMergeOptions, setShowMergeOptions] = useState(false); const [mergeMode, setMergeMode] = useState< - "overwrite" | "append" | "insertAt" - >("append"); - const [mergeInsertAt, setMergeInsertAt] = useState(""); + 'overwrite' | 'append' | 'insertAt' + >('append'); + const [mergeInsertAt, setMergeInsertAt] = useState(''); const { splitDownloads, @@ -123,7 +123,7 @@ const App: React.FC = () => { console.error(thrown); setError(message); }, - [], + [] ); const { thumbnails: reorderThumbnails, clearThumbnailCache } = @@ -154,7 +154,7 @@ const App: React.FC = () => { setWorkspaces(summaries); } catch (e) { console.error(e); - setError("Failed to read saved workspaces from browser storage."); + setError('Failed to read saved workspaces from browser storage.'); } }; @@ -165,7 +165,7 @@ const App: React.FC = () => { const resetWorkspaceState = () => { setPdf(null); setActiveWorkspaceId(null); - setWorkspaceName(""); + setWorkspaceName(''); resetWorkspaceCommandState(); clearGeneratedOutputs(); clearThumbnailCache(); @@ -183,7 +183,7 @@ const App: React.FC = () => { const workspaceId = activeWorkspaceId ?? createWorkspaceId(); const existing = workspaces.find( - (workspace) => workspace.id === workspaceId, + (workspace) => workspace.id === workspaceId ); const workspace: StoredWorkspace = { @@ -221,7 +221,7 @@ const App: React.FC = () => { } catch (e) { console.error(e); setError( - "Failed to save workspace. The browser storage quota may be full.", + 'Failed to save workspace. The browser storage quota may be full.' ); return false; } finally { @@ -242,7 +242,7 @@ const App: React.FC = () => { } openActionDialog({ - title: "Reset workspace?", + title: 'Reset workspace?', content: ( <>

This workspace has unsaved changes.

@@ -253,21 +253,21 @@ const App: React.FC = () => { ), actions: [ { - label: "Cancel", - variant: "secondary", + label: 'Cancel', + variant: 'secondary', onClick: closeActionDialog, }, { - label: "Reset without saving", - variant: "danger", + label: 'Reset without saving', + variant: 'danger', onClick: () => { closeActionDialog(); performResetWorkspace(); }, }, { - label: "Save and reset", - variant: "primary", + label: 'Save and reset', + variant: 'primary', autoFocus: true, onClick: async () => { closeActionDialog(); @@ -289,7 +289,7 @@ const App: React.FC = () => { const loaded = await loadWorkspaceFromIndexedDb(workspaceId); if (!loaded) { - setError("Workspace not found."); + setError('Workspace not found.'); await refreshWorkspaces(); return; } @@ -324,7 +324,7 @@ const App: React.FC = () => { setWorkspaceName(loaded.workspace.name); } catch (e) { console.error(e); - setError("Failed to load workspace from browser storage."); + setError('Failed to load workspace from browser storage.'); } finally { setIsBusy(false); } @@ -332,10 +332,10 @@ const App: React.FC = () => { const handleDeleteWorkspace = (workspaceId: string) => { const workspace = workspaces.find((item) => item.id === workspaceId); - const name = workspace?.name ?? "this workspace"; + const name = workspace?.name ?? 'this workspace'; openActionDialog({ - title: "Delete workspace?", + title: 'Delete workspace?', content: ( <>

@@ -350,13 +350,13 @@ const App: React.FC = () => { ), actions: [ { - label: "Cancel", - variant: "secondary", + label: 'Cancel', + variant: 'secondary', onClick: closeActionDialog, }, { - label: "Delete workspace", - variant: "danger", + label: 'Delete workspace', + variant: 'danger', autoFocus: true, onClick: () => { closeActionDialog(); @@ -377,14 +377,14 @@ const App: React.FC = () => { setActiveWorkspaceId(null); setWorkspaceDirty(true); setWorkspaceMessage( - "Saved workspace deleted. Current in-memory document remains open.", + 'Saved workspace deleted. Current in-memory document remains open.' ); } await refreshWorkspaces(); } catch (e) { console.error(e); - setError("Failed to delete workspace."); + setError('Failed to delete workspace.'); } }; @@ -411,7 +411,7 @@ const App: React.FC = () => { setWorkspaceName(defaultWorkspaceNameFromPdfName(loaded.name)); } catch (e) { console.error(e); - setError("Failed to load PDF (see console)."); + setError('Failed to load PDF (see console).'); } finally { setIsBusy(false); } @@ -423,7 +423,7 @@ const App: React.FC = () => { } else { setPendingFile(file); setShowMergeOptions(true); - setMergeMode("append"); + setMergeMode('append'); setMergeInsertAt(String(pages.length + 1)); } }; @@ -436,7 +436,7 @@ const App: React.FC = () => { const handleMergeConfirm = async () => { if (!pendingFile) return; - if (!pdf || mergeMode === "overwrite") { + if (!pdf || mergeMode === 'overwrite') { await loadFileAsNew(pendingFile); setPendingFile(null); setShowMergeOptions(false); @@ -464,12 +464,12 @@ const App: React.FC = () => { // 3) Determine insert position (0-based) let insertAt = pages.length; // default: append at end - if (mergeMode === "insertAt") { + if (mergeMode === 'insertAt') { const parsed = parseInt(mergeInsertAt, 10); if (Number.isFinite(parsed)) { insertAt = Math.min(Math.max(parsed - 1, 0), pages.length); } - } else if (mergeMode === "append") { + } else if (mergeMode === 'append') { insertAt = pages.length; } @@ -495,7 +495,7 @@ const App: React.FC = () => { setActiveWorkspaceId(null); } catch (e) { console.error(e); - setError("Failed to merge PDF (see console)."); + setError('Failed to merge PDF (see console).'); } finally { setIsBusy(false); setPendingFile(null); @@ -518,16 +518,16 @@ const App: React.FC = () => { const handleKeyDown = (e: KeyboardEvent) => { if (isEditableKeyboardTarget(e.target)) return; - if (e.key === "F1" || e.key === "?") { + if (e.key === 'F1' || e.key === '?') { e.preventDefault(); setHelpOpen(true); } }; - window.addEventListener("keydown", handleKeyDown); + window.addEventListener('keydown', handleKeyDown); return () => { - window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener('keydown', handleKeyDown); }; }, []); @@ -537,13 +537,13 @@ const App: React.FC = () => { const afterPages = pages.map((page) => page.id === pageId ? { ...page, rotation: (normalizeRotation(page.rotation) + 90) % 360 } - : page, + : page ); executeWorkspaceCommand( createWorkspaceCommand({ - type: "page.rotate", - label: "Rotated page clockwise", + type: 'page.rotate', + label: 'Rotated page clockwise', before, after: { ...before, @@ -553,7 +553,7 @@ const App: React.FC = () => { pageId, degrees: 90, }, - }), + }) ); }; @@ -562,13 +562,13 @@ const App: React.FC = () => { const afterPages = pages.map((page) => page.id === pageId ? { ...page, rotation: (normalizeRotation(page.rotation) + 270) % 360 } - : page, + : page ); executeWorkspaceCommand( createWorkspaceCommand({ - type: "page.rotate", - label: "Rotated page counterclockwise", + type: 'page.rotate', + label: 'Rotated page counterclockwise', before, after: { ...before, @@ -578,7 +578,7 @@ const App: React.FC = () => { pageId, degrees: -90, }, - }), + }) ); }; @@ -586,10 +586,10 @@ const App: React.FC = () => { const page = pages.find((item) => item.id === pageId); const visualIndex = page ? pages.indexOf(page) : -1; const pageLabel = - visualIndex >= 0 ? `page at position ${visualIndex + 1}` : "this page"; + visualIndex >= 0 ? `page at position ${visualIndex + 1}` : 'this page'; openActionDialog({ - title: "Delete page?", + title: 'Delete page?', content: (

Delete {pageLabel} from the current workspace? @@ -597,13 +597,13 @@ const App: React.FC = () => { ), actions: [ { - label: "Cancel", - variant: "secondary", + label: 'Cancel', + variant: 'secondary', onClick: closeActionDialog, }, { - label: "Delete page", - variant: "danger", + label: 'Delete page', + variant: 'danger', autoFocus: true, onClick: () => { closeActionDialog(); @@ -619,8 +619,8 @@ const App: React.FC = () => { executeWorkspaceCommand( createWorkspaceCommand({ - type: "page.delete", - label: "Deleted page", + type: 'page.delete', + label: 'Deleted page', before, after: { pages: pages.filter((page) => page.id !== pageId), @@ -630,7 +630,7 @@ const App: React.FC = () => { details: { pageId, }, - }), + }) ); }; @@ -639,8 +639,8 @@ const App: React.FC = () => { executeWorkspaceCommand( createWorkspaceCommand({ - type: "pages.reorder", - label: "Reordered pages", + type: 'pages.reorder', + label: 'Reordered pages', before, after: { ...before, @@ -649,14 +649,14 @@ const App: React.FC = () => { details: { pageCount: newPages.length, }, - }), + }) ); }; const handleToggleSelect = ( pageId: string, visualIndex: number, - e: React.MouseEvent, + e: React.MouseEvent ) => { setSelectedPageIds((prev) => { if (e.shiftKey && lastSelectedVisualIndex !== null && pages.length > 0) { @@ -731,28 +731,28 @@ const App: React.FC = () => { openActionDialog({ title: idsToDelete.length === 1 - ? "Delete selected page?" - : "Delete selected pages?", + ? 'Delete selected page?' + : 'Delete selected pages?', content: (

- Delete{" "} + Delete{' '} {idsToDelete.length === 1 - ? "1 selected page" + ? '1 selected page' : `${idsToDelete.length} selected pages`} - {" "} + {' '} from the current workspace?

), actions: [ { - label: "Cancel", - variant: "secondary", + label: 'Cancel', + variant: 'secondary', onClick: closeActionDialog, }, { - label: idsToDelete.length === 1 ? "Delete page" : "Delete pages", - variant: "danger", + label: idsToDelete.length === 1 ? 'Delete page' : 'Delete pages', + variant: 'danger', autoFocus: true, onClick: () => { closeActionDialog(); @@ -795,10 +795,10 @@ const App: React.FC = () => { executeWorkspaceCommand( createWorkspaceCommand({ - type: "pages.copy", + type: 'pages.copy', label: copiedPages.length === 1 - ? "Copied page" + ? 'Copied page' : `Copied ${copiedPages.length} pages`, before, after: { @@ -810,7 +810,7 @@ const App: React.FC = () => { count: copiedPages.length, insertSlot: clampedSlot, }, - }), + }) ); }; @@ -852,7 +852,7 @@ const App: React.FC = () => { const key = e.key.toLowerCase(); - if ((e.ctrlKey || e.metaKey) && key === "z") { + if ((e.ctrlKey || e.metaKey) && key === 'z') { e.preventDefault(); if (e.shiftKey) { handleRedo(); @@ -862,13 +862,13 @@ const App: React.FC = () => { return; } - if ((e.ctrlKey || e.metaKey) && key === "y") { + if ((e.ctrlKey || e.metaKey) && key === 'y') { e.preventDefault(); handleRedo(); return; } - if ((e.ctrlKey || e.metaKey) && key === "a") { + if ((e.ctrlKey || e.metaKey) && key === 'a') { e.preventDefault(); setSelectedPageIds(pages.map((page) => page.id)); setLastSelectedVisualIndex(null); @@ -876,7 +876,7 @@ const App: React.FC = () => { } if ( - (e.key === "Delete" || e.key === "Backspace") && + (e.key === 'Delete' || e.key === 'Backspace') && selectedPageIds.length > 0 ) { e.preventDefault(); @@ -884,17 +884,17 @@ const App: React.FC = () => { return; } - if (e.key === "Escape" && selectedPageIds.length > 0) { + if (e.key === 'Escape' && selectedPageIds.length > 0) { e.preventDefault(); setSelectedPageIds([]); setLastSelectedVisualIndex(null); } }; - window.addEventListener("keydown", handleKeyDown); + window.addEventListener('keydown', handleKeyDown); return () => { - window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener('keydown', handleKeyDown); }; }, [ hasPdf, @@ -919,7 +919,7 @@ const App: React.FC = () => { replaceSplitResults(result); } catch (e) { console.error(e); - setError("Error while splitting PDF (see console)."); + setError('Error while splitting PDF (see console).'); } finally { setIsBusy(false); } @@ -939,18 +939,17 @@ const App: React.FC = () => { if (selectedPages.length === 0) return; const blob = await exportPages(pdf, selectedPages); - const base = pdf.name.replace(/\.pdf$/i, ""); + const base = pdf.name.replace(/\.pdf$/i, ''); const filename = `${base}_selected.pdf`; replaceSubsetResult(blob, filename); } catch (e) { console.error(e); - setError("Error while extracting selected pages (see console)."); + setError('Error while extracting selected pages (see console).'); } finally { setIsBusy(false); } }; - const performOpenSelectionAsWorkspace = async () => { if (!pdf || selectedPageIds.length === 0) return; @@ -988,7 +987,7 @@ const App: React.FC = () => { redoHistory: [], dirty: true, message: `Created a new workspace from ${selectedPageCount} selected ${ - selectedPageCount === 1 ? "page" : "pages" + selectedPageCount === 1 ? 'page' : 'pages' }.`, }); @@ -999,7 +998,7 @@ const App: React.FC = () => { clearThumbnailCache(); } catch (e) { console.error(e); - setError("Error while opening selection as a new workspace."); + setError('Error while opening selection as a new workspace.'); } finally { setIsBusy(false); } @@ -1017,13 +1016,13 @@ const App: React.FC = () => { } openActionDialog({ - title: "Open selection as new workspace?", + title: 'Open selection as new workspace?', content: ( <>

This will replace the current in-memory workspace with a new workspace built from {selectedPages.length}{' '} - {selectedPages.length === 1 ? "selected page" : "selected pages"}. + {selectedPages.length === 1 ? 'selected page' : 'selected pages'}.

The current workspace has unsaved changes. Do you want to save it @@ -1033,21 +1032,21 @@ const App: React.FC = () => { ), actions: [ { - label: "Cancel", - variant: "secondary", + label: 'Cancel', + variant: 'secondary', onClick: closeActionDialog, }, { - label: "Open without saving", - variant: "danger", + label: 'Open without saving', + variant: 'danger', onClick: () => { closeActionDialog(); void performOpenSelectionAsWorkspace(); }, }, { - label: "Save and open", - variant: "primary", + label: 'Save and open', + variant: 'primary', autoFocus: true, onClick: async () => { closeActionDialog(); @@ -1068,12 +1067,12 @@ const App: React.FC = () => { try { const blob = await exportPages(pdf, pages); - const base = pdf.name.replace(/\.pdf$/i, ""); + const base = pdf.name.replace(/\.pdf$/i, ''); const filename = `${base}_reordered.pdf`; replaceExportResult(blob, filename); } catch (e) { console.error(e); - setError("Error while exporting reordered PDF (see console)."); + setError('Error while exporting reordered PDF (see console).'); } finally { setIsBusy(false); } @@ -1120,62 +1119,62 @@ const App: React.FC = () => { {showMergeOptions && pendingFile && pdf && pages.length > 0 && (

Open file: merge or replace?

-

- You already have {pdf.name} with {pages.length}{" "} - pages open. What should happen with{" "} +

+ You already have {pdf.name} with {pages.length}{' '} + pages open. What should happen with{' '} {pendingFile.name}?

-
+
@@ -1240,7 +1239,7 @@ const App: React.FC = () => { selectedCount={selectedPageIds.length} onSplit={handleSplit} onExtractSelected={handleExtractSelected} - onOpenSelectionAsWorkspace={handleOpenSelectionAsWorkspace} + onOpenSelectionAsWorkspace={handleOpenSelectionAsWorkspace} onExportReordered={handleExportReordered} splitDownloads={splitDownloads} subsetDownload={subsetDownload} @@ -1250,7 +1249,7 @@ const App: React.FC = () => { {error && (
Error: {error}
@@ -1272,7 +1271,7 @@ const App: React.FC = () => { diff --git a/src/components/ActionDialog.tsx b/src/components/ActionDialog.tsx index bb50f8c..167258f 100644 --- a/src/components/ActionDialog.tsx +++ b/src/components/ActionDialog.tsx @@ -1,9 +1,9 @@ -import React, { useEffect } from "react"; +import React, { useEffect } from 'react'; export interface ActionDialogAction { label: string; onClick: () => void | Promise; - variant?: "primary" | "secondary" | "danger"; + variant?: 'primary' | 'secondary' | 'danger'; disabled?: boolean; autoFocus?: boolean; title?: string; @@ -18,21 +18,21 @@ interface ActionDialogProps { } const backgroundByVariant: Record< - NonNullable, + NonNullable, string > = { - primary: "#2563eb", - secondary: "#e5e7eb", - danger: "#dc2626", + primary: '#2563eb', + secondary: '#e5e7eb', + danger: '#dc2626', }; const colorByVariant: Record< - NonNullable, + NonNullable, string > = { - primary: "white", - secondary: "#111827", - danger: "white", + primary: 'white', + secondary: '#111827', + danger: 'white', }; const ActionDialog: React.FC = ({ @@ -46,16 +46,16 @@ const ActionDialog: React.FC = ({ if (!open) return; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { + if (e.key === 'Escape') { e.preventDefault(); onClose(); } }; - window.addEventListener("keydown", handleKeyDown); + window.addEventListener('keydown', handleKeyDown); return () => { - window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener('keydown', handleKeyDown); }; }, [open, onClose]); @@ -72,42 +72,42 @@ const ActionDialog: React.FC = ({ } }} style={{ - position: "fixed", + position: 'fixed', inset: 0, zIndex: 70, - background: "rgba(15, 23, 42, 0.55)", - display: "flex", - alignItems: "center", - justifyContent: "center", - padding: "1rem", + background: 'rgba(15, 23, 42, 0.55)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '1rem', }} >

{title} @@ -117,18 +117,18 @@ const ActionDialog: React.FC = ({ type="button" onClick={onClose} style={{ - border: "none", - borderRadius: "999px", - width: "1.8rem", - height: "1.8rem", - background: "#e5e7eb", - color: "#111827", - cursor: "pointer", - fontSize: "1.1rem", + border: 'none', + borderRadius: '999px', + width: '1.8rem', + height: '1.8rem', + background: '#e5e7eb', + color: '#111827', + cursor: 'pointer', + fontSize: '1.1rem', lineHeight: 1, - display: "flex", - alignItems: "center", - justifyContent: "center", + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }} aria-label="Close dialog" > @@ -138,8 +138,8 @@ const ActionDialog: React.FC = ({
@@ -148,15 +148,15 @@ const ActionDialog: React.FC = ({
{actions.map((action) => { - const variant = action.variant ?? "secondary"; + const variant = action.variant ?? 'secondary'; return ( @@ -69,11 +69,11 @@ const ActionsPanel: React.FC = ({ className="secondary" disabled={disabled || selectedCount === 0} onClick={handleExtractSelectedClick} - style={{ flex: "1 1 45%" }} + style={{ flex: '1 1 45%' }} title={ selectedCount === 0 - ? "Select at least one page" - : "Create a PDF from selected pages" + ? 'Select at least one page' + : 'Create a PDF from selected pages' } > 📤 Extract selected ({selectedCount}) @@ -97,15 +97,15 @@ const ActionsPanel: React.FC = ({ className="secondary" disabled={disabled} onClick={onSplit} - style={{ flex: "1 1 45%" }} + style={{ flex: '1 1 45%' }} > 📂 Split into single PDFs
{subsetDownload && ( -
- Subset result:{" "} +
+ Subset result:{' '} = ({ )} {exportDownload && ( -
- Exported document:{" "} +
+ Exported document:{' '} = ({ )} {splitDownloads.length > 0 && ( -
+
Single-page PDFs:
{splitDownloads.map((download) => ( diff --git a/src/components/FileLoader.tsx b/src/components/FileLoader.tsx index b18793e..08d2af8 100644 --- a/src/components/FileLoader.tsx +++ b/src/components/FileLoader.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import type { PdfFile } from "../pdf/pdfTypes"; +import React from 'react'; +import type { PdfFile } from '../pdf/pdfTypes'; interface FileLoaderProps { pdf: PdfFile | null; @@ -11,7 +11,7 @@ const FileLoader: React.FC = ({ pdf, onFileLoaded }) => { const file = e.target.files?.[0]; if (file) { onFileLoaded(file); - e.target.value = ""; + e.target.value = ''; } }; @@ -22,7 +22,7 @@ const FileLoader: React.FC = ({ pdf, onFileLoaded }) => { {pdf && ( -
+
Loaded: {pdf.name}
diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index 041443b..6209b56 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect } from 'react'; interface HelpDialogProps { open: boolean; @@ -6,55 +6,55 @@ interface HelpDialogProps { } const shortcuts = [ - { keys: "F1 / ?", description: "Open this help and tutorial dialog" }, + { keys: 'F1 / ?', description: 'Open this help and tutorial dialog' }, { - keys: "Ctrl/⌘ + A", - description: "Select all pages in the current workspace", + keys: 'Ctrl/⌘ + A', + description: 'Select all pages in the current workspace', }, { - keys: "Delete / Backspace", - description: "Delete the selected pages after confirmation", + keys: 'Delete / Backspace', + description: 'Delete the selected pages after confirmation', }, { - keys: "Esc", - description: "Clear the page selection or close an open dialog", + keys: 'Esc', + description: 'Clear the page selection or close an open dialog', }, - { keys: "Ctrl/⌘ + Z", description: "Undo the latest workspace command" }, + { keys: 'Ctrl/⌘ + Z', description: 'Undo the latest workspace command' }, { - keys: "Ctrl/⌘ + Shift + Z", - description: "Redo the next workspace command", + keys: 'Ctrl/⌘ + Shift + Z', + description: 'Redo the next workspace command', }, - { keys: "Ctrl/⌘ + Y", description: "Redo the next workspace command" }, + { keys: 'Ctrl/⌘ + Y', description: 'Redo the next workspace command' }, { - keys: "← / → in preview", - description: "Move to the previous or next page in the preview overlay", + keys: '← / → in preview', + description: 'Move to the previous or next page in the preview overlay', }, ]; const tutorialSteps = [ { - title: "1. Open a PDF or load a workspace", - body: "Start by selecting a local PDF file. If you saved workspaces before, you can restore one from browser storage instead.", + title: '1. Open a PDF or load a workspace', + body: 'Start by selecting a local PDF file. If you saved workspaces before, you can restore one from browser storage instead.', }, { - title: "2. Arrange pages visually", - body: "Drag page cards to reorder them. Rotate single pages, open the large preview with a click, or remove pages you do not want in the export.", + title: '2. Arrange pages visually', + body: 'Drag page cards to reorder them. Rotate single pages, open the large preview with a click, or remove pages you do not want in the export.', }, { - title: "3. Select, copy, and delete pages", - body: "Use the checkbox on a page card to select it. Shift-click extends a range. Dragging a selected page moves the whole selection; the copy controls duplicate selected pages into a chosen slot.", + title: '3. Select, copy, and delete pages', + body: 'Use the checkbox on a page card to select it. Shift-click extends a range. Dragging a selected page moves the whole selection; the copy controls duplicate selected pages into a chosen slot.', }, { - title: "4. Extract selected pages or branch into a new workspace", - body: "Extract selected pages when you only need a download. Open the selection as a new workspace when you want to continue working on that subset.", + title: '4. Extract selected pages or branch into a new workspace', + body: 'Extract selected pages when you only need a download. Open the selection as a new workspace when you want to continue working on that subset.', }, { - title: "5. Save your workspace or export a PDF", - body: "Saving a workspace keeps the current working state in this browser. Exporting creates a new PDF file for download.", + title: '5. Save your workspace or export a PDF', + body: 'Saving a workspace keeps the current working state in this browser. Exporting creates a new PDF file for download.', }, { - title: "6. Use history deliberately", - body: "Each workspace operation is stored as a command with label and timestamp. Undo and redo walk through that command history.", + title: '6. Use history deliberately', + body: 'Each workspace operation is stored as a command with label and timestamp. Undo and redo walk through that command history.', }, ]; @@ -63,7 +63,7 @@ const HelpDialog: React.FC = ({ open, onClose }) => { if (!open) return; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key !== "Escape") return; + if (e.key !== 'Escape') return; e.preventDefault(); e.stopPropagation(); @@ -71,10 +71,10 @@ const HelpDialog: React.FC = ({ open, onClose }) => { onClose(); }; - window.addEventListener("keydown", handleKeyDown, { capture: true }); + window.addEventListener('keydown', handleKeyDown, { capture: true }); return () => { - window.removeEventListener("keydown", handleKeyDown, { capture: true }); + window.removeEventListener('keydown', handleKeyDown, { capture: true }); }; }, [open, onClose]); diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 23e745e..885a55c 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { APP_VERSION } from "../version"; +import React from 'react'; +import { APP_VERSION } from '../version'; interface LayoutProps { children: React.ReactNode; diff --git a/src/components/PagePreviewModal.tsx b/src/components/PagePreviewModal.tsx index 2325467..5430dc3 100644 --- a/src/components/PagePreviewModal.tsx +++ b/src/components/PagePreviewModal.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useRef } from "react"; -import type { PdfFile } from "../pdf/pdfTypes"; -import * as pdfjsLib from "pdfjs-dist"; -import pdfjsWorker from "pdfjs-dist/build/pdf.worker?worker&url"; +import React, { useEffect, useRef } from 'react'; +import type { PdfFile } from '../pdf/pdfTypes'; +import * as pdfjsLib from 'pdfjs-dist'; +import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url'; // pdf.js worker setup // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -43,28 +43,28 @@ const PagePreviewModal: React.FC = ({ if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { + if (e.key === 'Escape') { e.preventDefault(); onClose(); return; } - if (e.key === "ArrowLeft" && canGoPrevious) { + if (e.key === 'ArrowLeft' && canGoPrevious) { e.preventDefault(); onPrevious(); return; } - if (e.key === "ArrowRight" && canGoNext) { + if (e.key === 'ArrowRight' && canGoNext) { e.preventDefault(); onNext(); } }; - window.addEventListener("keydown", handleKeyDown); + window.addEventListener('keydown', handleKeyDown); return () => { - window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener('keydown', handleKeyDown); }; }, [isOpen, canGoPrevious, canGoNext, onPrevious, onNext, onClose]); @@ -77,7 +77,7 @@ const PagePreviewModal: React.FC = ({ try { const canvas = canvasRef.current; if (canvas) { - const ctx = canvas.getContext("2d"); + const ctx = canvas.getContext('2d'); if (ctx) { ctx.clearRect(0, 0, canvas.width, canvas.height); } @@ -102,7 +102,7 @@ const PagePreviewModal: React.FC = ({ const scale = Math.min( maxWidth / viewport.width, - maxHeight / viewport.height, + maxHeight / viewport.height ); const scaledViewport = page.getViewport({ scale }); @@ -110,7 +110,7 @@ const PagePreviewModal: React.FC = ({ const visibleCanvas = canvasRef.current; if (!visibleCanvas) return; - const visibleCtx = visibleCanvas.getContext("2d"); + const visibleCtx = visibleCanvas.getContext('2d'); if (!visibleCtx) return; let canvasWidth = scaledViewport.width; @@ -126,14 +126,15 @@ const PagePreviewModal: React.FC = ({ visibleCanvas.width = canvasWidth; visibleCanvas.height = canvasHeight; - const baseCanvas = document.createElement("canvas"); - const baseCtx = baseCanvas.getContext("2d"); + const baseCanvas = document.createElement('canvas'); + const baseCtx = baseCanvas.getContext('2d'); if (!baseCtx) return; baseCanvas.width = scaledViewport.width; baseCanvas.height = scaledViewport.height; const renderTask = page.render({ + canvas: baseCanvas, canvasContext: baseCtx, viewport: scaledViewport, }); @@ -161,7 +162,7 @@ const PagePreviewModal: React.FC = ({ visibleCtx.drawImage(baseCanvas, 0, 0); visibleCtx.restore(); } catch (e) { - console.error("Error rendering preview", e); + console.error('Error rendering preview', e); } })(); @@ -181,30 +182,30 @@ const PagePreviewModal: React.FC = ({
e.stopPropagation()} style={{ - position: "relative", - background: "#111827", - borderRadius: "0.75rem", - padding: "0.75rem", - maxWidth: "90vw", - maxHeight: "90vh", - display: "flex", - flexDirection: "column", - alignItems: "center", - gap: "0.5rem", - overflow: "visible", + position: 'relative', + background: '#111827', + borderRadius: '0.75rem', + padding: '0.75rem', + maxWidth: '90vw', + maxHeight: '90vh', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '0.5rem', + overflow: 'visible', }} > {/* Previous page */} @@ -216,22 +217,22 @@ const PagePreviewModal: React.FC = ({ }} disabled={!canGoPrevious} style={{ - position: "absolute", + position: 'absolute', left: 0, - top: "50%", - transform: "translate(-50%, -50%)", - width: "2.5rem", - height: "2.5rem", - borderRadius: "999px", - border: "none", - background: canGoPrevious ? "#374151" : "#1f2937", - color: canGoPrevious ? "#e5e7eb" : "#6b7280", - cursor: canGoPrevious ? "pointer" : "default", - fontSize: "1.35rem", + top: '50%', + transform: 'translate(-50%, -50%)', + width: '2.5rem', + height: '2.5rem', + borderRadius: '999px', + border: 'none', + background: canGoPrevious ? '#374151' : '#1f2937', + color: canGoPrevious ? '#e5e7eb' : '#6b7280', + cursor: canGoPrevious ? 'pointer' : 'default', + fontSize: '1.35rem', lineHeight: 1, - display: "flex", - alignItems: "center", - justifyContent: "center", + display: 'flex', + alignItems: 'center', + justifyContent: 'center', zIndex: 2, }} title="Previous page (←)" @@ -249,22 +250,22 @@ const PagePreviewModal: React.FC = ({ }} disabled={!canGoNext} style={{ - position: "absolute", + position: 'absolute', right: 0, - top: "50%", - transform: "translate(50%, -50%)", - width: "2.5rem", - height: "2.5rem", - borderRadius: "999px", - border: "none", - background: canGoNext ? "#374151" : "#1f2937", - color: canGoNext ? "#e5e7eb" : "#6b7280", - cursor: canGoNext ? "pointer" : "default", - fontSize: "1.35rem", + top: '50%', + transform: 'translate(50%, -50%)', + width: '2.5rem', + height: '2.5rem', + borderRadius: '999px', + border: 'none', + background: canGoNext ? '#374151' : '#1f2937', + color: canGoNext ? '#e5e7eb' : '#6b7280', + cursor: canGoNext ? 'pointer' : 'default', + fontSize: '1.35rem', lineHeight: 1, - display: "flex", - alignItems: "center", - justifyContent: "center", + display: 'flex', + alignItems: 'center', + justifyContent: 'center', zIndex: 2, }} title="Next page (→)" @@ -281,22 +282,22 @@ const PagePreviewModal: React.FC = ({ onClose(); }} style={{ - position: "absolute", + position: 'absolute', top: 0, right: 0, - transform: "translate(50%, -50%)", - width: "2.25rem", - height: "2.25rem", - borderRadius: "999px", - border: "none", - background: "#374151", - color: "#e5e7eb", - cursor: "pointer", - fontSize: "1.2rem", + transform: 'translate(50%, -50%)', + width: '2.25rem', + height: '2.25rem', + borderRadius: '999px', + border: 'none', + background: '#374151', + color: '#e5e7eb', + cursor: 'pointer', + fontSize: '1.2rem', lineHeight: 1, - display: "flex", - alignItems: "center", - justifyContent: "center", + display: 'flex', + alignItems: 'center', + justifyContent: 'center', zIndex: 3, }} title="Close preview (Esc)" @@ -308,14 +309,14 @@ const PagePreviewModal: React.FC = ({ -
+
{positionLabel} · Original page {pageIndex + 1} · Rot {rotation}°
diff --git a/src/components/PageWorkspace/CopyPagesDialog.tsx b/src/components/PageWorkspace/CopyPagesDialog.tsx index 6b4275a..19b89e0 100644 --- a/src/components/PageWorkspace/CopyPagesDialog.tsx +++ b/src/components/PageWorkspace/CopyPagesDialog.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect } from 'react'; interface CopyPagesDialogProps { selectedCount: number; @@ -21,16 +21,16 @@ const CopyPagesDialog: React.FC = ({ }) => { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { + if (e.key === 'Escape') { e.preventDefault(); onCancel(); } }; - window.addEventListener("keydown", handleKeyDown); + window.addEventListener('keydown', handleKeyDown); return () => { - window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener('keydown', handleKeyDown); }; }, [onCancel]); @@ -45,43 +45,43 @@ const CopyPagesDialog: React.FC = ({ } }} style={{ - position: "fixed", + position: 'fixed', inset: 0, zIndex: 60, - background: "rgba(15, 23, 42, 0.55)", - display: "flex", - alignItems: "center", - justifyContent: "center", - padding: "1rem", + background: 'rgba(15, 23, 42, 0.55)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '1rem', }} >

Copy selected pages @@ -91,18 +91,18 @@ const CopyPagesDialog: React.FC = ({ type="button" onClick={onCancel} style={{ - border: "none", - borderRadius: "999px", - width: "1.8rem", - height: "1.8rem", - background: "#e5e7eb", - color: "#111827", - cursor: "pointer", - fontSize: "1.1rem", + border: 'none', + borderRadius: '999px', + width: '1.8rem', + height: '1.8rem', + background: '#e5e7eb', + color: '#111827', + cursor: 'pointer', + fontSize: '1.1rem', lineHeight: 1, - display: "flex", - alignItems: "center", - justifyContent: "center", + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }} aria-label="Close copy dialog" > @@ -113,25 +113,25 @@ const CopyPagesDialog: React.FC = ({

- Copy{" "} + Copy{' '} {selectedCount === 1 - ? "1 selected page" + ? '1 selected page' : `${selectedCount} selected pages`} - {" "} + {' '} to a new position.

@@ -165,12 +165,12 @@ const CopyPagesDialog: React.FC = ({ {error && (
{error} @@ -179,23 +179,23 @@ const CopyPagesDialog: React.FC = ({
@@ -98,7 +98,7 @@ const WorkspacePanel: React.FC = ({ className="secondary" onClick={onRedo} disabled={!hasPdf || isBusy || !canRedo} - title={latestRedo ? `Redo: ${latestRedo.label}` : "Nothing to redo"} + title={latestRedo ? `Redo: ${latestRedo.label}` : 'Nothing to redo'} > ↷ Redo @@ -108,9 +108,9 @@ const WorkspacePanel: React.FC = ({ className="secondary" onClick={onSaveWorkspace} disabled={!hasPdf || isBusy} - title={!hasPdf ? "Open a PDF first" : "Save workspace"} + title={!hasPdf ? 'Open a PDF first' : 'Save workspace'} > - 💾 {activeWorkspaceId ? "Save" : "Save as"} + 💾 {activeWorkspaceId ? 'Save' : 'Save as'}