Compare commits
4 Commits
feat/62-do
...
c9b4e6dad4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9b4e6dad4 | ||
|
|
8519fbb48a | ||
|
|
ee85ce4668 | ||
|
|
ecfd80bf9a |
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user