feat(frontend): move annotation comments to right-side panel
Annotation threads now open in a slide-in side panel (320 px, right edge of the PDF viewer) instead of expanding the bottom drawer. The PDF stays visible while the user reads and writes annotation comments. - Add AnnotationSidePanel component (absolute-positioned, CSS slide transition, keyed CommentThread, close via X or Escape) - Remove the $effect that opened the bottom drawer on annotation click - Simplify PanelDiscussion back to document-level thread only (no annotation sub-tabs) - Remove annotation-related props from DocumentBottomPanel and PanelDiscussion Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
65
frontend/src/lib/components/AnnotationSidePanel.svelte
Normal file
65
frontend/src/lib/components/AnnotationSidePanel.svelte
Normal file
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import CommentThread from './CommentThread.svelte';
|
||||
|
||||
type Props = {
|
||||
documentId: string;
|
||||
activeAnnotationId: string | null;
|
||||
activeAnnotationPage: number | null;
|
||||
canComment: boolean;
|
||||
currentUserId: string | null;
|
||||
canAdmin: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
let {
|
||||
documentId,
|
||||
activeAnnotationId,
|
||||
activeAnnotationPage,
|
||||
canComment,
|
||||
currentUserId,
|
||||
canAdmin,
|
||||
onClose
|
||||
}: Props = $props();
|
||||
|
||||
const visible = $derived(activeAnnotationId !== null);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 right-0 z-10 flex w-80 flex-col border-l border-brand-sand bg-white shadow-[-4px_0_16px_rgba(0,0,0,0.08)] transition-transform duration-200 {visible
|
||||
? 'pointer-events-auto translate-x-0'
|
||||
: 'translate-x-full'}"
|
||||
data-testid="annotation-side-panel"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex shrink-0 items-center justify-between border-b border-brand-sand px-4 py-3">
|
||||
<span class="font-sans text-xs font-medium text-brand-navy">
|
||||
{m.doc_panel_discussion_annotation_tab({ page: String(activeAnnotationPage ?? '?') })}
|
||||
</span>
|
||||
<button
|
||||
onclick={onClose}
|
||||
aria-label={m.comment_panel_close()}
|
||||
class="rounded p-1 text-gray-400 transition-colors hover:bg-brand-sand/50 hover:text-brand-navy"
|
||||
>
|
||||
<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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Comment thread -->
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
{#if activeAnnotationId}
|
||||
{#key activeAnnotationId}
|
||||
<CommentThread
|
||||
documentId={documentId}
|
||||
annotationId={activeAnnotationId}
|
||||
canComment={canComment}
|
||||
currentUserId={currentUserId}
|
||||
canAdmin={canAdmin}
|
||||
loadOnMount={true}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,9 +48,6 @@ type Props = {
|
||||
open: boolean;
|
||||
height: number;
|
||||
activeTab: Tab;
|
||||
activeAnnotationId: string | null;
|
||||
activeAnnotationPage: number | null;
|
||||
onAnnotationCommentCountChange?: (annotationId: string, count: number) => void;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -61,10 +58,7 @@ let {
|
||||
canAdmin,
|
||||
open = $bindable(),
|
||||
height = $bindable(),
|
||||
activeTab = $bindable(),
|
||||
activeAnnotationId,
|
||||
activeAnnotationPage,
|
||||
onAnnotationCommentCountChange
|
||||
activeTab = $bindable()
|
||||
}: Props = $props();
|
||||
|
||||
const MIN_HEIGHT = 52; // drag handle (8px) + tab bar (~44px)
|
||||
@@ -187,13 +181,10 @@ const panelHeight = $derived(open ? height : MIN_HEIGHT);
|
||||
{:else if activeTab === 'discussion'}
|
||||
<PanelDiscussion
|
||||
documentId={doc.id}
|
||||
activeAnnotationId={activeAnnotationId}
|
||||
activeAnnotationPage={activeAnnotationPage}
|
||||
initialComments={comments}
|
||||
canComment={canComment}
|
||||
currentUserId={currentUserId}
|
||||
canAdmin={canAdmin}
|
||||
onAnnotationCommentCountChange={onAnnotationCommentCountChange}
|
||||
/>
|
||||
{:else if activeTab === 'history'}
|
||||
<PanelHistory documentId={doc.id} />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import CommentThread from './CommentThread.svelte';
|
||||
|
||||
type CommentReply = {
|
||||
@@ -23,107 +22,21 @@ type Comment = {
|
||||
|
||||
type Props = {
|
||||
documentId: string;
|
||||
activeAnnotationId: string | null;
|
||||
activeAnnotationPage: number | null;
|
||||
initialComments: Comment[];
|
||||
canComment: boolean;
|
||||
currentUserId: string | null;
|
||||
canAdmin: boolean;
|
||||
onAnnotationCommentCountChange?: (annotationId: string, count: number) => void;
|
||||
};
|
||||
|
||||
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';
|
||||
}
|
||||
let { documentId, initialComments, canComment, currentUserId, canAdmin }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Sub-tab bar (only shown when annotation is active) -->
|
||||
{#if activeAnnotationId}
|
||||
<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_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}
|
||||
annotationId={activeAnnotationId}
|
||||
canComment={canComment}
|
||||
currentUserId={currentUserId}
|
||||
canAdmin={canAdmin}
|
||||
loadOnMount={true}
|
||||
onCountChange={(count) => onAnnotationCommentCountChange?.(activeAnnotationId, count)}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<CommentThread
|
||||
documentId={documentId}
|
||||
initialComments={initialComments}
|
||||
canComment={canComment}
|
||||
currentUserId={currentUserId}
|
||||
canAdmin={canAdmin}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { onMount } from 'svelte';
|
||||
import DocumentTopBar from '$lib/components/DocumentTopBar.svelte';
|
||||
import DocumentViewer from '$lib/components/DocumentViewer.svelte';
|
||||
import DocumentBottomPanel from '$lib/components/DocumentBottomPanel.svelte';
|
||||
import AnnotationSidePanel from '$lib/components/AnnotationSidePanel.svelte';
|
||||
|
||||
type Tab = 'metadata' | 'transcription' | 'discussion' | 'history';
|
||||
|
||||
@@ -58,14 +59,6 @@ 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(() => {
|
||||
if (activeAnnotationId) {
|
||||
activeTab = 'discussion';
|
||||
panelOpen = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Close the panel when entering annotate mode so the PDF is fully visible.
|
||||
$effect(() => {
|
||||
if (annotateMode) panelOpen = false;
|
||||
@@ -152,6 +145,18 @@ $effect(() => {
|
||||
activeAnnotationId = id;
|
||||
}}
|
||||
/>
|
||||
<AnnotationSidePanel
|
||||
documentId={doc.id}
|
||||
activeAnnotationId={activeAnnotationId}
|
||||
activeAnnotationPage={activeAnnotationPage}
|
||||
canComment={canComment}
|
||||
currentUserId={currentUserId}
|
||||
canAdmin={canAdmin}
|
||||
onClose={() => {
|
||||
activeAnnotationId = null;
|
||||
activeAnnotationPage = null;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DocumentBottomPanel
|
||||
@@ -163,7 +168,5 @@ $effect(() => {
|
||||
bind:open={panelOpen}
|
||||
bind:height={panelHeight}
|
||||
bind:activeTab={activeTab}
|
||||
activeAnnotationId={activeAnnotationId}
|
||||
activeAnnotationPage={activeAnnotationPage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user