Compare commits

..

4 Commits

Author SHA1 Message Date
Marcel
c9b4e6dad4 feat(frontend): add annotation visibility toggle to PDF toolbar
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m27s
CI / Backend Unit Tests (pull_request) Successful in 2m6s
CI / E2E Tests (pull_request) Failing after 26m28s
Eye/eye-slash button in the PDF controls bar lets the user hide all
annotation highlights to read the document unobstructed and show them again
with one click.

- Button only renders when at least one annotation exists
- Active state (hidden) highlighted with brand-mint/bg-white/10 so the
  current state is always clear
- i18n keys added for de/en/es

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 22:59:53 +01:00
Marcel
8519fbb48a fix(frontend): lock document page to viewport with position: fixed
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m20s
CI / Backend Unit Tests (pull_request) Successful in 2m11s
CI / E2E Tests (pull_request) Failing after 26m7s
The global layout wraps pages in min-h-screen + main.py-6, which pushed
the h-screen document container below the sticky nav and caused page-level
scrolling. Switching to fixed inset-0 z-50 fully escapes the layout flow:

- DocumentTopBar always visible (no scrolling it away)
- PDF controls always visible
- Only the PDF canvas area scrolls
- DocumentBottomPanel moved inside the fixed container (logically grouped)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 22:53:02 +01:00
Marcel
ee85ce4668 feat(frontend): keep annotation tab after switching to document discussion
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / E2E Tests (pull_request) Failing after 26m48s
CI / Unit & Component Tests (pull_request) Successful in 2m29s
CI / Backend Unit Tests (pull_request) Successful in 2m16s
Clicking the Diskussion sub-tab no longer deselects the active annotation,
so the Annotation tab stays visible and accessible for easy toggling back.

The annotation is cleared only via Escape or clicking elsewhere on the PDF.
Removes the now-unused onClearAnnotation callback chain.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 22:49:44 +01:00
Marcel
ecfd80bf9a feat(frontend): add discussion sub-tab navigation for annotation threads
Some checks failed
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m34s
CI / Backend Unit Tests (pull_request) Successful in 2m16s
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (pull_request) Failing after 24m11s
Within the Diskussion panel tab, show two sub-tabs when an annotation is
active: «Diskussion» (document-level thread, with comment-count badge) and
«Annotation · Seite N» (annotation-specific thread).

Behaviour:
- Clicking an annotation auto-switches to the Annotation sub-tab
- Clicking the Diskussion sub-tab deselects the annotation and returns to
  the document thread
- Escape clears the active annotation (or collapses the panel if none)
- activeAnnotationPage is now lifted from PdfViewer → DocumentViewer →
  page → DocumentBottomPanel → PanelDiscussion so the tab label shows the
  correct page number

Closes #60
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 22:45:35 +01:00
8 changed files with 181 additions and 50 deletions

View File

@@ -257,5 +257,8 @@
"doc_panel_tab_history": "Verlauf",
"doc_panel_annotate": "Annotieren",
"doc_panel_annotate_stop": "Fertig",
"doc_panel_annotation_thread_title": "Annotation"
"doc_panel_annotation_thread_title": "Annotation",
"doc_panel_discussion_annotation_tab": "Annotation · Seite {page}",
"pdf_annotations_show": "Annotierungen anzeigen",
"pdf_annotations_hide": "Annotierungen verbergen"
}

View File

@@ -257,5 +257,8 @@
"doc_panel_tab_history": "History",
"doc_panel_annotate": "Annotate",
"doc_panel_annotate_stop": "Done",
"doc_panel_annotation_thread_title": "Annotation"
"doc_panel_annotation_thread_title": "Annotation",
"doc_panel_discussion_annotation_tab": "Annotation · Page {page}",
"pdf_annotations_show": "Show annotations",
"pdf_annotations_hide": "Hide annotations"
}

View File

@@ -257,5 +257,8 @@
"doc_panel_tab_history": "Historial",
"doc_panel_annotate": "Anotar",
"doc_panel_annotate_stop": "Listo",
"doc_panel_annotation_thread_title": "Anotación"
"doc_panel_annotation_thread_title": "Anotación",
"doc_panel_discussion_annotation_tab": "Anotación · Página {page}",
"pdf_annotations_show": "Mostrar anotaciones",
"pdf_annotations_hide": "Ocultar anotaciones"
}

View File

@@ -49,6 +49,7 @@ type Props = {
height: number;
activeTab: Tab;
activeAnnotationId: string | null;
activeAnnotationPage: number | null;
onAnnotationCommentCountChange?: (annotationId: string, count: number) => void;
};
@@ -62,6 +63,7 @@ let {
height = $bindable(),
activeTab = $bindable(),
activeAnnotationId,
activeAnnotationPage,
onAnnotationCommentCountChange
}: Props = $props();
@@ -182,6 +184,7 @@ const panelHeight = $derived(open ? height : MIN_HEIGHT);
<PanelDiscussion
documentId={doc.id}
activeAnnotationId={activeAnnotationId}
activeAnnotationPage={activeAnnotationPage}
initialComments={comments}
canComment={canComment}
currentUserId={currentUserId}

View File

@@ -15,6 +15,7 @@ type Props = {
error: string;
annotateMode: boolean;
activeAnnotationId: string | null;
activeAnnotationPage: number | null;
onAnnotationClick: (id: string) => void;
};
@@ -25,6 +26,7 @@ let {
error,
annotateMode = $bindable(),
activeAnnotationId = $bindable(),
activeAnnotationPage = $bindable(),
onAnnotationClick
}: Props = $props();
</script>
@@ -79,6 +81,7 @@ let {
documentId={doc.id}
bind:annotateMode={annotateMode}
bind:activeAnnotationId={activeAnnotationId}
bind:activeAnnotationPage={activeAnnotationPage}
onAnnotationClick={onAnnotationClick}
/>
{:else if fileUrl}

View File

@@ -24,6 +24,7 @@ type Comment = {
type Props = {
documentId: string;
activeAnnotationId: string | null;
activeAnnotationPage: number | null;
initialComments: Comment[];
canComment: boolean;
currentUserId: string | null;
@@ -34,23 +35,84 @@ type Props = {
let {
documentId,
activeAnnotationId,
activeAnnotationPage,
initialComments,
canComment,
currentUserId,
canAdmin,
onAnnotationCommentCountChange
}: Props = $props();
// Sub-tab within the discussion panel: 'document' or 'annotation'
type DiscussionTab = 'document' | 'annotation';
let activeSubTab = $state<DiscussionTab>('document');
// Track document-level comment count for badge.
// CommentThread calls onCountChange immediately on mount with the accurate total.
let docCommentCount = $state(0);
// When an annotation becomes active, switch to the annotation sub-tab automatically.
$effect(() => {
if (activeAnnotationId) {
activeSubTab = 'annotation';
} else {
activeSubTab = 'document';
}
});
function selectDocumentTab() {
activeSubTab = 'document';
}
function selectAnnotationTab() {
activeSubTab = 'annotation';
}
</script>
<div class="space-y-8 p-6">
<!-- Annotation thread (shown when an annotation is active) -->
<div class="flex h-full flex-col">
<!-- Sub-tab bar (only shown when annotation is active) -->
{#if activeAnnotationId}
<div>
<h4
class="mb-3 border-b border-brand-sand pb-2 font-sans text-xs font-bold tracking-widest text-brand-navy uppercase"
<div class="flex shrink-0 border-b border-brand-sand/70 bg-gray-50 px-4">
<button
onclick={selectDocumentTab}
class="mr-1 px-3 py-2 font-sans text-xs font-medium transition-colors {activeSubTab === 'document'
? 'border-b-2 border-brand-navy text-brand-navy'
: 'text-gray-400 hover:text-brand-navy'}"
>
{m.doc_panel_annotation_thread_title()}
</h4>
{m.doc_panel_tab_discussion()}
{#if docCommentCount > 0}
<span
class="ml-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-brand-mint px-1 font-mono text-[10px] text-brand-navy"
>
{docCommentCount}
</span>
{/if}
</button>
<button
onclick={selectAnnotationTab}
class="px-3 py-2 font-sans text-xs font-medium transition-colors {activeSubTab === 'annotation'
? 'border-b-2 border-brand-navy text-brand-navy'
: 'text-gray-400 hover:text-brand-navy'}"
>
{m.doc_panel_discussion_annotation_tab({ page: String(activeAnnotationPage ?? '?') })}
</button>
</div>
{/if}
<!-- Content area -->
<div class="flex-1 overflow-y-auto p-6">
{#if !activeAnnotationId || activeSubTab === 'document'}
<!-- Document-level thread -->
<CommentThread
documentId={documentId}
initialComments={initialComments}
canComment={canComment}
currentUserId={currentUserId}
canAdmin={canAdmin}
onCountChange={(count) => (docCommentCount = count)}
/>
{:else}
<!-- Annotation-level thread -->
{#key activeAnnotationId}
<CommentThread
documentId={documentId}
@@ -62,24 +124,6 @@ let {
onCountChange={(count) => onAnnotationCommentCountChange?.(activeAnnotationId, count)}
/>
{/key}
</div>
{/if}
<!-- General document discussion -->
<div>
{#if activeAnnotationId}
<h4
class="mb-3 border-b border-brand-sand pb-2 font-sans text-xs font-bold tracking-widest text-brand-navy uppercase"
>
{m.comment_section_title()}
</h4>
{/if}
<CommentThread
documentId={documentId}
initialComments={initialComments}
canComment={canComment}
currentUserId={currentUserId}
canAdmin={canAdmin}
/>
</div>
</div>

View File

@@ -3,18 +3,21 @@ import { onMount } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';
import type { PDFDocumentProxy, PDFPageProxy, RenderTask } from 'pdfjs-dist';
import AnnotationLayer from './AnnotationLayer.svelte';
import { m } from '$lib/paraglide/messages.js';
let {
url,
documentId = '',
annotateMode = $bindable(false),
activeAnnotationId = $bindable<string | null>(null),
activeAnnotationPage = $bindable<number | null>(null),
onAnnotationClick
}: {
url: string;
documentId?: string;
annotateMode?: boolean;
activeAnnotationId?: string | null;
activeAnnotationPage?: number | null;
onAnnotationClick?: (id: string) => void;
} = $props();
@@ -53,6 +56,7 @@ type Annotation = {
let annotations = $state<Annotation[]>([]);
let annotateColor = $state('#ffff00');
let commentCounts = new SvelteMap<string, number>();
let showAnnotations = $state(true);
onMount(async () => {
// Dynamic import keeps pdfjs out of the SSR bundle entirely
@@ -213,6 +217,7 @@ async function handleAnnotationDraw(rect: { x: number; y: number; width: number;
const created: Annotation = await res.json();
annotations = [...annotations, created];
activeAnnotationId = created.id;
activeAnnotationPage = created.pageNumber;
onAnnotationClick?.(created.id);
}
} catch {
@@ -236,6 +241,8 @@ async function handleAnnotationDelete(annotationId: string) {
function handleAnnotationClick(id: string) {
activeAnnotationId = id;
const ann = annotations.find((a) => a.id === id);
activeAnnotationPage = ann?.pageNumber ?? null;
onAnnotationClick?.(id);
}
@@ -398,6 +405,53 @@ function zoomOut() {
{/if}
</div>
<!-- Annotation visibility toggle (shown when annotations exist) -->
{#if annotations.length > 0}
<button
onclick={() => (showAnnotations = !showAnnotations)}
aria-label={showAnnotations ? m.pdf_annotations_hide() : m.pdf_annotations_show()}
title={showAnnotations ? m.pdf_annotations_hide() : m.pdf_annotations_show()}
class="rounded p-1 transition {showAnnotations
? 'text-gray-300 hover:bg-white/10'
: 'bg-white/10 text-brand-mint'}"
>
{#if showAnnotations}
<svg
class="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
{:else}
<svg
class="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg>
{/if}
</button>
{/if}
<!-- PDF canvas area -->
<div class="relative flex-1 overflow-auto">
{#if loading}
@@ -419,15 +473,17 @@ function zoomOut() {
class="textLayer"
style="position: absolute; top: 0; left: 0; overflow: hidden; pointer-events: none; line-height: 1;"
></div>
<AnnotationLayer
annotations={annotations.filter((a) => a.pageNumber === currentPage)}
canAnnotate={annotateMode}
color={annotateColor}
onDraw={handleAnnotationDraw}
onDelete={handleAnnotationDelete}
commentCounts={Object.fromEntries(commentCounts)}
onAnnotationClick={handleAnnotationClick}
/>
{#if showAnnotations}
<AnnotationLayer
annotations={annotations.filter((a) => a.pageNumber === currentPage)}
canAnnotate={annotateMode}
color={annotateColor}
onDraw={handleAnnotationDraw}
onDelete={handleAnnotationDelete}
commentCounts={Object.fromEntries(commentCounts)}
onAnnotationClick={handleAnnotationClick}
/>
{/if}
</div>
</div>
{/if}

View File

@@ -56,6 +56,7 @@ async function loadFile(id: string) {
let annotateMode = $state(false);
let activeAnnotationId = $state<string | null>(null);
let activeAnnotationPage = $state<number | null>(null);
// When an annotation is clicked, open the Diskussion tab.
$effect(() => {
@@ -97,6 +98,19 @@ onMount(() => {
}
localStorageRestored = true;
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
if (activeAnnotationId) {
activeAnnotationId = null;
activeAnnotationPage = null;
} else if (panelOpen) {
panelOpen = false;
}
}
}
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
});
// Persist panel state whenever it changes (after initial restore).
@@ -112,7 +126,7 @@ $effect(() => {
<title>{doc.title || doc.originalFilename || 'Dokument'}</title>
</svelte:head>
<div class="flex h-screen flex-col overflow-hidden bg-white" data-hydrated>
<div class="fixed inset-0 z-50 flex flex-col overflow-hidden bg-white" data-hydrated>
<DocumentTopBar
doc={doc}
canWrite={data.canWrite ?? false}
@@ -129,21 +143,23 @@ $effect(() => {
error={fileError}
bind:annotateMode={annotateMode}
bind:activeAnnotationId={activeAnnotationId}
bind:activeAnnotationPage={activeAnnotationPage}
onAnnotationClick={(id) => {
activeAnnotationId = id;
}}
/>
</div>
</div>
<DocumentBottomPanel
doc={doc}
comments={(data.comments ?? []) as never[]}
canComment={canComment}
currentUserId={currentUserId}
canAdmin={canAdmin}
bind:open={panelOpen}
bind:height={panelHeight}
bind:activeTab={activeTab}
activeAnnotationId={activeAnnotationId}
/>
<DocumentBottomPanel
doc={doc}
comments={(data.comments ?? []) as never[]}
canComment={canComment}
currentUserId={currentUserId}
canAdmin={canAdmin}
bind:open={panelOpen}
bind:height={panelHeight}
bind:activeTab={activeTab}
activeAnnotationId={activeAnnotationId}
activeAnnotationPage={activeAnnotationPage}
/>
</div>