diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f9da649..01833d1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,7 +7,7 @@ import type { InspectorTab } from './components/InspectorPanel'; import { SignatureModal } from './components/SignatureModal'; import { RedactPagesModal } from './components/RedactPagesModal'; import { AboutModal } from './components/AboutModal'; -import { ToastViewport } from './components/ui'; + import { CustomConfirmationModal } from './components/custom/CustomConfirmationModal'; import type { CustomConfirmationOptions } from './components/custom/CustomConfirmationModal'; import { PDFViewer } from './viewer/PDFViewer'; @@ -19,7 +19,7 @@ import { PasswordModal } from './components/PasswordModal'; import type { DocumentInfo, SearchResult, EditOperation, DocumentMetadata, FontInfo, OutlineItem, PDFPermissions } from './lib/gatewayService'; import { viewportRectToPdf } from './lib/coordinateMapping'; import type { Rect } from './lib/coordinateMapping'; -import { toast } from './lib/toast'; + import { DEFAULT_TOOL_SETTINGS, TOOL_SHORTCUTS } from './lib/tools'; import type { ToolId, ToolSettings, StampPreset } from './lib/tools'; @@ -39,8 +39,7 @@ function App() { const permissions = activeDoc?.permissions ?? null; const can = (flag: keyof PDFPermissions) => !permissions || permissions[flag] !== false; - const denyToast = (label: string) => - toast(`${label} is not permitted by this document's restrictions`, 'error'); + const denyToast = (_label: string) => {}; const disabledTools = new Set(); if (!can('canAnnotate')) (['highlight', 'underline', 'strikeout', 'squiggly', 'draw', 'comment', 'textbox', 'stamp', 'signature'] as ToolId[]).forEach((t) => disabledTools.add(t)); @@ -86,15 +85,21 @@ function App() { if (!canUndo) return; preservePageRef.current = true; setHist((h) => ({ ...h, index: Math.max(0, h.index - 1) })); - toast('Undo', 'info', 1200); + }; const redo = () => { if (!canRedo) return; preservePageRef.current = true; setHist((h) => ({ ...h, index: Math.min(h.stack.length - 1, h.index + 1) })); - toast('Redo', 'info', 1200); + }; + useEffect(() => { + if (hist.stack.length > 0) { + localStorage.setItem('pdf_hist', JSON.stringify(hist)); + } + }, [hist]); + useEffect(() => { gatewayService.getHealth() .then((h) => { setBackendHealthy(true); setEngineReady(!!h.engine_available); }) @@ -107,7 +112,23 @@ function App() { setIsLoading(true); const docs = await gatewayService.listDocuments(); setDocuments(docs); - if (docs.length > 0) openDocument(docs[0].id); + if (docs.length > 0) { + // Attempt to load from localStorage, otherwise fallback to the most recent document + const savedHist = localStorage.getItem('pdf_hist'); + if (savedHist) { + try { + const parsedHist = JSON.parse(savedHist); + if (parsedHist.stack && parsedHist.stack.length > 0 && docs.some((d: any) => d.id === parsedHist.stack[parsedHist.index])) { + setHist(parsedHist); + return; + } + } catch (e) { + console.error('Failed to parse history', e); + } + } + // Default to the most recent document (last in list) + openDocument(docs[docs.length - 1].id); + } } catch (e) { console.error('Failed to load documents', e); } finally { @@ -217,18 +238,16 @@ function App() { gatewayService.listDocuments().then(setDocuments).catch(() => {}); }; - const applyOps = async (ops: EditOperation[], successMsg?: string) => { + const applyOps = async (ops: EditOperation[], _successMsg?: string) => { if (!selectedDocId) return; setIsSaving(true); try { const result = await gatewayService.applyEdits(selectedDocId, ops); if (result.success) { adoptNewDocument(result.newDocumentId); - if (successMsg) toast(successMsg, 'success'); } } catch (e) { console.error('Edit failed', e); - toast('Edit failed — check the gateway connection', 'error'); } finally { setIsSaving(false); } @@ -398,9 +417,7 @@ function App() { if (newRedactions.length > 0) { setPendingRedactions(p => [...p, ...newRedactions]); - toast(`Marked ${newRedactions.length} page${newRedactions.length > 1 ? 's' : ''} for redaction`, 'success'); } else { - toast('No valid pages found in range', 'error'); } setRedactPagesModalOpen(false); }; @@ -424,7 +441,7 @@ function App() { const handleDeletePage = (pageIndex: number) => { if (!activeDoc) return; if (!can('canAssemble')) { denyToast('Deleting pages'); return; } - if (activeDoc.totalPages <= 1) { toast('Cannot delete the only page', 'error'); return; } + if (activeDoc.totalPages <= 1) { return; } setConfirmState({ title: 'Delete page?', message: `Page ${pageIndex + 1} will be removed from this document.`, @@ -479,14 +496,13 @@ function App() { const newDoc = await gatewayService.uploadDocument(file, password); setDocuments((prev) => [newDoc, ...prev]); openDocument(newDoc.id); - toast(`Opened ${newDoc.filename}`, 'success'); + setPasswordPrompt(null); } catch (e) { if (e instanceof PasswordError) { setPasswordPrompt({ file, filename: file.name, error: password ? 'Incorrect password — please try again.' : undefined }); } else { console.error('Upload failed', e); - toast('Upload failed', 'error'); } } finally { setIsLoading(false); @@ -498,10 +514,8 @@ function App() { if (!can('canCopy')) { denyToast('Exporting'); return; } try { await gatewayService.exportDocument(selectedDocId, activeDoc.filename); - toast('Exported', 'success'); } catch (e) { console.error('Export failed', e); - toast('Export failed', 'error'); } }; @@ -509,7 +523,7 @@ function App() { if (!activeDoc) return; if (!can('canPrint')) { denyToast('Printing'); return; } try { - toast('Preparing print...', 'info'); + const bytes = await gatewayService.fetchDocumentBytes(selectedDocId); const blob = new Blob([bytes], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); @@ -527,7 +541,6 @@ function App() { document.body.appendChild(iframe); } catch (e) { console.error('Print failed', e); - toast('Print failed', 'error'); } }; @@ -752,7 +765,6 @@ function App() { setPendingSignature({ url, aspect }); setSignatureModalOpen(false); setActiveTool('signature'); - toast('Signature ready — click on the page to place it', 'info'); }} /> @@ -774,7 +786,6 @@ function App() { onSubmit={(pw) => { if (passwordPrompt) handleUpload(passwordPrompt.file, pw); }} onClose={() => setPasswordPrompt(null)} /> - ); } diff --git a/frontend/src/components/SignatureModal.tsx b/frontend/src/components/SignatureModal.tsx index ee75a00..de64b34 100644 --- a/frontend/src/components/SignatureModal.tsx +++ b/frontend/src/components/SignatureModal.tsx @@ -1,6 +1,6 @@ import { CustomButton } from './custom/CustomButton'; import React, { useRef, useState, useEffect, useCallback } from 'react'; -import { toast } from '../lib/toast'; + /* ─── Types ─────────────────────────────────────────────── */ interface SignatureModalProps { @@ -224,19 +224,19 @@ export const SignatureModal: React.FC = ({ open, onClose, o let aspect = 3; if (mode === 'draw') { - if (!hasInk.current) { toast('Draw your signature first', 'error'); return; } + if (!hasInk.current) return; const c = canvasRef.current!; dataUrl = c.toDataURL('image/png'); aspect = c.width / c.height; } else if (mode === 'type') { - if (!typed.trim()) { toast('Type your name first', 'error'); return; } + if (!typed.trim()) return; const res = buildTypedCanvas(); dataUrl = res.dataUrl; aspect = res.aspect; } else if (mode === 'upload') { - if (!uploaded) { toast('Upload a signature image first', 'error'); return; } + if (!uploaded) return; dataUrl = uploaded.url; aspect = uploaded.aspect; } else if (mode === 'saved') { - toast('Click "Use" on a saved signature', 'error'); return; + return; } saveToStorage(dataUrl, aspect); diff --git a/frontend/src/components/ToolRail.tsx b/frontend/src/components/ToolRail.tsx index 9411fbe..0b12786 100644 --- a/frontend/src/components/ToolRail.tsx +++ b/frontend/src/components/ToolRail.tsx @@ -1,7 +1,7 @@ import { CustomButton } from './custom/CustomButton'; import React from 'react'; import type { ToolId } from '../lib/tools'; -import { toast } from '../lib/toast'; + import { Popover } from './ui'; import { SelectIcon, PanIcon, HighlightIcon, DrawIcon, CommentIcon, TextBoxIcon, @@ -86,7 +86,6 @@ const RailButton: React.FC<{ t: ToolDef; active: boolean; disabled?: boolean; on export const ToolRail: React.FC = ({ activeTool, onToolChange, hasSignature, onOpenSignature, onOpenAbout, disabledTools }) => { const pickTool = (id: ToolId) => { if (disabledTools?.has(id)) { - toast("This tool is not permitted by this document's restrictions", 'error'); return; } onToolChange(id); diff --git a/frontend/src/components/ui.tsx b/frontend/src/components/ui.tsx index 4208022..5fb0742 100644 --- a/frontend/src/components/ui.tsx +++ b/frontend/src/components/ui.tsx @@ -1,7 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { subscribeToasts, dismissToast } from '../lib/toast'; -import type { ToastItem } from '../lib/toast'; -import { XIcon, CheckIcon, InfoIcon } from './icons'; +import { XIcon } from './icons'; import { CustomButton } from './custom/CustomButton'; interface PopoverProps { @@ -189,30 +187,4 @@ export const ConfirmDialog: React.FC = ({ state, onClose })

{state?.message}

); - -const toastStyles: Record = { - info: 'border-[#ebedf0] bg-[#18212e] text-white', - success: 'border-transparent bg-[#16a34a] text-white', - error: 'border-transparent bg-[#dc2626] text-white', -}; -export const ToastViewport: React.FC = () => { - const [items, setItems] = useState([]); - useEffect(() => subscribeToasts(setItems), []); - return ( -
- {items.map((t) => ( -
- {t.kind === 'success' && } - {t.kind === 'error' && } - {t.kind === 'info' && } - {t.message} - dismissToast(t.id)}> -
- ))} -
- ); -}; \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/viewer/PDFViewer.tsx b/frontend/src/viewer/PDFViewer.tsx index 0e2e6a8..6d1def0 100644 --- a/frontend/src/viewer/PDFViewer.tsx +++ b/frontend/src/viewer/PDFViewer.tsx @@ -12,7 +12,7 @@ import type { Rect } from '../lib/coordinateMapping'; import { gatewayService } from '../lib/gatewayService'; import type { SearchResult, PageInfo, Glyph } from '../lib/gatewayService'; import type { ToolSettings, ToolId, StampPreset } from '../lib/tools'; -import { toast } from '../lib/toast'; + import { RedactionLayer } from './RedactionLayer'; import { StreamEditLayer } from './StreamEditLayer'; import { wasmFreeDocument } from '../lib/pdfiumEngine'; @@ -325,17 +325,9 @@ export const PDFViewer = React.forwardRef(({ const handleTextSelection = (text: string, bbox: Rect, lines: Rect[], pageIndex: number) => { // This is still called on Ctrl+C for select mode if (activeTool === 'select') { - if (!canCopy) { - toast("Copying is not permitted by this document's restrictions", 'error'); - return; - } + if (!canCopy) return; if (text.trim()) { - navigator.clipboard?.writeText(text).then( - () => toast(`Copied ${text.length} character${text.length > 1 ? 's' : ''}`, 'success'), - () => toast('Copy failed — clipboard unavailable', 'error'), - ); - } else { - toast('No selectable text in that area', 'info'); + navigator.clipboard?.writeText(text).catch(() => {}); } return; } @@ -386,14 +378,8 @@ export const PDFViewer = React.forwardRef(({ const { pageIndex, text, bbox, lines } = textSelection; switch (action) { case 'copy': - if (!canCopy) { - toast("Copying is not permitted by this document's restrictions", 'error'); - break; - } - navigator.clipboard?.writeText(text).then( - () => toast(`Copied ${text.length} character${text.length > 1 ? 's' : ''}`, 'success'), - () => toast('Copy failed — clipboard unavailable', 'error') - ); + if (!canCopy) break; + navigator.clipboard?.writeText(text).catch(() => {}); break; case 'highlight': { const newAnno: Annotation = { @@ -432,7 +418,7 @@ export const PDFViewer = React.forwardRef(({ }); break; case 'edit': - toast('Please select the Edit Text tool from the toolbar to edit text content', 'info'); + break; case 'comment': { const newAnno: Annotation = { diff --git a/frontend/src/viewer/StreamEditLayer.tsx b/frontend/src/viewer/StreamEditLayer.tsx index cbc766a..a26c588 100644 --- a/frontend/src/viewer/StreamEditLayer.tsx +++ b/frontend/src/viewer/StreamEditLayer.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useRef } from 'react'; import { gatewayService } from '../lib/gatewayService'; import type { TextObjectResponse } from '../lib/gatewayService'; -import { toast } from '../lib/toast'; + import { loadPdfFont } from '../lib/fontFaceLoader'; @@ -90,7 +90,7 @@ export const StreamEditLayer: React.FC = ({ if (newText === obj.text) return; try { const res = await gatewayService.updateTextObject(documentId, pageIndex, idx, newText); - toast('Text updated successfully', 'success'); + if (res.newDocumentId && onDocumentChanged) { onDocumentChanged(res.newDocumentId); } else { @@ -102,7 +102,7 @@ export const StreamEditLayer: React.FC = ({ onEditSuccess(); } } catch (e: any) { - toast(`Failed to update text: ${e.message}`, 'error'); + console.error('Failed to update text:', e.message); } };