From 5fb6a1eec08275b6390f41ed98c153a9bb5a82c5 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 23 Mar 2026 19:10:02 +0100 Subject: [PATCH 01/10] feat(frontend): replace iframe with PDF.js viewer (#39) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Install pdfjs-dist v5 and add optimizeDeps pre-bundle config - New PdfViewer.svelte component: renders each page on a with correct device-pixel-ratio scaling, overlays a text layer (enables text selection; foundation for annotations in #40), prev/next navigation, zoom controls, and lazy page rendering (only current ±1 pre-fetched — avoids freezing on multi-page documents) - Replace the + {:else if fileUrl && doc.contentType?.startsWith('application/pdf')} + {:else if fileUrl}
{m.doc_image_alt()} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 76be0fe1..c5f71a26 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -6,6 +6,9 @@ import { playwright } from '@vitest/browser-playwright'; import { sveltekit } from '@sveltejs/kit/vite'; export default defineConfig({ + optimizeDeps: { + include: ['pdfjs-dist'] + }, server: { host: '0.0.0.0', // Erlaubt Zugriff von außen port: 5173, // Standard SvelteKit Port diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 386ec56f..c75d7088 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -207,6 +207,28 @@ resolved "https://registry.npmjs.org/@lix-js/server-protocol-schema/-/server-protocol-schema-0.1.1.tgz" integrity sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ== +"@napi-rs/canvas-linux-x64-gnu@0.1.97": + version "0.1.97" + resolved "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.97.tgz" + integrity sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg== + +"@napi-rs/canvas@^0.1.95": + version "0.1.97" + resolved "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz" + integrity sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ== + optionalDependencies: + "@napi-rs/canvas-android-arm64" "0.1.97" + "@napi-rs/canvas-darwin-arm64" "0.1.97" + "@napi-rs/canvas-darwin-x64" "0.1.97" + "@napi-rs/canvas-linux-arm-gnueabihf" "0.1.97" + "@napi-rs/canvas-linux-arm64-gnu" "0.1.97" + "@napi-rs/canvas-linux-arm64-musl" "0.1.97" + "@napi-rs/canvas-linux-riscv64-gnu" "0.1.97" + "@napi-rs/canvas-linux-x64-gnu" "0.1.97" + "@napi-rs/canvas-linux-x64-musl" "0.1.97" + "@napi-rs/canvas-win32-arm64-msvc" "0.1.97" + "@napi-rs/canvas-win32-x64-msvc" "0.1.97" + "@playwright/test@^1.58.2": version "1.58.2" resolved "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz" @@ -1462,6 +1484,11 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +node-readable-to-web-readable-stream@^0.4.2: + version "0.4.2" + resolved "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz" + integrity sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ== + obug@^2.1.0, obug@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz" @@ -1553,6 +1580,14 @@ pathe@^2.0.3: resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz" integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== +pdfjs-dist@^5.5.207: + version "5.5.207" + resolved "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz" + integrity sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw== + optionalDependencies: + "@napi-rs/canvas" "^0.1.95" + node-readable-to-web-readable-stream "^0.4.2" + picocolors@^1.0.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" -- 2.49.1 From 1ad8fffd1b335ba966b4a8e6eb42d3099294a1e4 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 23 Mar 2026 22:30:54 +0100 Subject: [PATCH 02/10] fix(frontend): load pdfjs-dist dynamically to avoid SSR crash (#39) Static import of pdfjs-dist fails during SSR because DOMMatrix and other browser globals are unavailable in Node.js. Move the import into onMount so it only ever executes in the browser. A plain pdfjsLib variable holds the module; a $state boolean pdfjsReady triggers the load-document effect once the library is available. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/PdfViewer.svelte | 32 ++++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/frontend/src/lib/components/PdfViewer.svelte b/frontend/src/lib/components/PdfViewer.svelte index 224b2cc6..d22ca2b4 100644 --- a/frontend/src/lib/components/PdfViewer.svelte +++ b/frontend/src/lib/components/PdfViewer.svelte @@ -1,9 +1,6 @@ + +
+ {#each annotations as annotation (annotation.id)} +
+ {#if canAnnotate} + + {/if} +
+ {/each} + + {#if drawRect && drawRect.width > 0} +
+ {/if} +
diff --git a/frontend/src/lib/components/AnnotationLayer.svelte.spec.ts b/frontend/src/lib/components/AnnotationLayer.svelte.spec.ts new file mode 100644 index 00000000..e25f95c4 --- /dev/null +++ b/frontend/src/lib/components/AnnotationLayer.svelte.spec.ts @@ -0,0 +1,74 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; + +import AnnotationLayer from './AnnotationLayer.svelte'; + +afterEach(cleanup); + +type Annotation = { + id: string; + documentId: string; + pageNumber: number; + x: number; + y: number; + width: number; + height: number; + color: string; + createdAt: string; +}; + +function makeAnnotation(id = 'ann-1'): Annotation { + return { + id, + documentId: 'doc-1', + pageNumber: 1, + x: 0.1, + y: 0.1, + width: 0.3, + height: 0.2, + color: '#ff0000', + createdAt: new Date().toISOString() + }; +} + +describe('AnnotationLayer', () => { + it('renders a colored element for each annotation', async () => { + render(AnnotationLayer, { + annotations: [makeAnnotation('ann-1'), makeAnnotation('ann-2')], + canAnnotate: false, + color: '#ff0000', + onDraw: () => {}, + onDelete: () => {} + }); + + await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument(); + await expect.element(page.getByTestId('annotation-ann-2')).toBeInTheDocument(); + }); + + it('shows a delete button for each annotation when canAnnotate is true', async () => { + render(AnnotationLayer, { + annotations: [makeAnnotation('ann-1')], + canAnnotate: true, + color: '#ff0000', + onDraw: () => {}, + onDelete: () => {} + }); + + await expect + .element(page.getByRole('button', { name: /annotation löschen/i })) + .toBeInTheDocument(); + }); + + it('does not show delete buttons when canAnnotate is false', async () => { + render(AnnotationLayer, { + annotations: [makeAnnotation('ann-1')], + canAnnotate: false, + color: '#ff0000', + onDraw: () => {}, + onDelete: () => {} + }); + + expect(page.getByRole('button', { name: /annotation löschen/i }).query()).toBeNull(); + }); +}); diff --git a/frontend/src/lib/components/PdfViewer.svelte b/frontend/src/lib/components/PdfViewer.svelte index 6514b707..7863143c 100644 --- a/frontend/src/lib/components/PdfViewer.svelte +++ b/frontend/src/lib/components/PdfViewer.svelte @@ -1,8 +1,17 @@