Phase 3 implementation. Coordinate system implemented. Initial text box tool implemented.
Some checks failed
Automated Container Build / build-and-push (push) Failing after 4s
CI / Backend (Python) (push) Failing after 17s
CI / Frontend (TypeScript) (push) Failing after 4m48s

This commit is contained in:
Elijah 2026-06-12 16:30:07 -07:00
parent c159ad4f37
commit 4cb038ec78
34 changed files with 6676 additions and 130 deletions

View file

@ -0,0 +1,88 @@
import { useEffect, useRef, useState } from 'react';
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import { AnnotationLayer } from '../canvas/AnnotationLayer';
interface PageRendererProps {
pdfDoc: PDFDocumentProxy;
pageNumber: number;
scale: number;
dpr: number;
}
export function PageRenderer({ pdfDoc, pageNumber, scale, dpr }: PageRendererProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [page, setPage] = useState<PDFPageProxy | null>(null);
useEffect(() => {
let active = true;
pdfDoc.getPage(pageNumber).then(p => {
if (active) setPage(p);
});
return () => { active = false; };
}, [pdfDoc, pageNumber]);
useEffect(() => {
if (!page || !canvasRef.current) return;
const viewport = page.getViewport({ scale: scale * dpr });
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
if (!context) return;
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.width = `${viewport.width / dpr}px`;
canvas.style.height = `${viewport.height / dpr}px`;
const renderContext = {
canvas: canvas,
viewport: viewport,
};
let renderTask = page.render(renderContext);
return () => {
renderTask.cancel();
};
}, [page, scale, dpr]);
if (!page) {
return (
<div
className="bg-white shadow-sm flex items-center justify-center text-neutral-400 border border-neutral-200"
style={{ width: 612 * scale, height: 792 * scale }}
>
Loading page {pageNumber}...
</div>
);
}
const baseViewport = page ? page.getViewport({ scale: 1, rotation: 0 }) : null;
return (
<div className="relative shadow-xl bg-white border border-neutral-200 group">
<canvas ref={canvasRef} className="block" />
{baseViewport && (
<AnnotationLayer
pageNumber={pageNumber}
width={baseViewport.width * scale}
height={baseViewport.height * scale}
viewportParams={{
scale,
rotation: page.rotate,
canonicalWidth: baseViewport.width,
canonicalHeight: baseViewport.height,
dpr
}}
/>
)}
<div className="absolute -left-10 top-2 text-xs font-bold text-neutral-400 bg-white/80 rounded-md px-1.5 py-0.5 shadow-sm border border-neutral-200">
{pageNumber}
</div>
</div>
);
}

View file

@ -0,0 +1,32 @@
import type { PDFDocumentProxy } from 'pdfjs-dist';
import { PageRenderer } from './PageRenderer';
import { useEditorStore } from '../store';
interface PageStackProps {
pdfDoc: PDFDocumentProxy;
}
export function PageStack({ pdfDoc }: PageStackProps) {
const numPages = pdfDoc.numPages;
const pages = Array.from({ length: numPages }, (_, i) => i + 1);
const zoom = useEditorStore(state => state.zoom);
// For now, render all pages vertically. Virtualization comes in Phase 7.
const scale = zoom * 2.0;
const dpr = window.devicePixelRatio || 1;
return (
<div className="flex flex-col items-center gap-8 py-8 w-full max-w-full">
{pages.map(pageNum => (
<PageRenderer
key={pageNum}
pdfDoc={pdfDoc}
pageNumber={pageNum}
scale={scale}
dpr={dpr}
/>
))}
</div>
);
}

View file

@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
import { PageStack } from './PageStack';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.mjs',
import.meta.url
).toString();
export function PdfDocument({ documentId }: { documentId: string }) {
const [pdfDoc, setPdfDoc] = useState<pdfjsLib.PDFDocumentProxy | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let active = true;
const url = `/api/v1/documents/${documentId}/file`;
const loadingTask = pdfjsLib.getDocument({ url });
loadingTask.promise.then((doc) => {
if (active) setPdfDoc(doc);
}).catch(err => {
console.error('Failed to load PDF', err);
if (active) setError(err.message);
});
return () => {
active = false;
loadingTask.destroy();
};
}, [documentId]);
if (error) {
return (
<div className="flex items-center justify-center h-full w-full">
<div className="text-red-500 bg-red-50 px-4 py-3 rounded-lg border border-red-200">
Error loading PDF: {error}
</div>
</div>
);
}
if (!pdfDoc) {
return (
<div className="flex flex-col items-center justify-center h-full w-full gap-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accent-500"></div>
<div className="text-neutral-500 font-medium animate-pulse">Loading document...</div>
</div>
);
}
return <PageStack pdfDoc={pdfDoc} />;
}