0 ? 'border-t border-line pt-4' : ''}>
diff --git a/frontend/src/lib/components/CommentThread.svelte.spec.ts b/frontend/src/lib/components/CommentThread.svelte.spec.ts
new file mode 100644
index 00000000..a444a7ce
--- /dev/null
+++ b/frontend/src/lib/components/CommentThread.svelte.spec.ts
@@ -0,0 +1,70 @@
+import { describe, it, expect, vi, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import CommentThread from './CommentThread.svelte';
+import type { Comment } from '$lib/types';
+
+afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+});
+
+function makeComment(id: string, content = 'Hello'): Comment {
+ return {
+ id,
+ authorId: 'user-1',
+ authorName: 'Alice',
+ content,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ replies: []
+ };
+}
+
+const baseProps = {
+ documentId: 'doc-1',
+ canComment: true,
+ currentUserId: 'user-1',
+ canAdmin: false
+};
+
+describe('CommentThread – empty state', () => {
+ it('shows empty state hint when there are no comments', async () => {
+ render(CommentThread, { ...baseProps, initialComments: [] });
+ await expect
+ .element(page.getByText('Noch keine Kommentare – starte die Diskussion!'))
+ .toBeInTheDocument();
+ });
+
+ it('does not show empty state hint when comments exist', async () => {
+ render(CommentThread, { ...baseProps, initialComments: [makeComment('c-1')] });
+ await expect
+ .element(page.getByText('Noch keine Kommentare – starte die Diskussion!'))
+ .not.toBeInTheDocument();
+ });
+});
+
+describe('CommentThread – onCountChange', () => {
+ it('calls onCountChange with initial SSR count on mount', async () => {
+ const onCountChange = vi.fn();
+ render(CommentThread, {
+ ...baseProps,
+ initialComments: [makeComment('c-1'), makeComment('c-2')],
+ onCountChange
+ });
+ expect(onCountChange).toHaveBeenCalledWith(2);
+ });
+
+ it('calls onCountChange with 0 when no initial comments', async () => {
+ const onCountChange = vi.fn();
+ render(CommentThread, { ...baseProps, initialComments: [], onCountChange });
+ expect(onCountChange).toHaveBeenCalledWith(0);
+ });
+
+ it('counts replies in the total', async () => {
+ const onCountChange = vi.fn();
+ const comment = { ...makeComment('c-1'), replies: [makeComment('r-1') as never] };
+ render(CommentThread, { ...baseProps, initialComments: [comment], onCountChange });
+ expect(onCountChange).toHaveBeenCalledWith(2);
+ });
+});
diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte b/frontend/src/lib/components/DocumentBottomPanel.svelte
index 94acb09d..7213d7c4 100644
--- a/frontend/src/lib/components/DocumentBottomPanel.svelte
+++ b/frontend/src/lib/components/DocumentBottomPanel.svelte
@@ -98,10 +98,16 @@ const tabs: { id: DocumentPanelTab; label: () => string }[] = [
];
const panelHeight = $derived(open ? height : MIN_HEIGHT);
+
+let discussionCount = $state((() => comments.reduce((s, c) => s + 1 + c.replies.length, 0))());
+
+function handleCountChange(count: number) {
+ discussionCount = count;
+}
@@ -120,6 +126,15 @@ const panelHeight = $derived(open ? height : MIN_HEIGHT);
+ {#if !open && discussionCount === 0}
+
+ {/if}
+
{#each tabs as tab (tab.id)}
@@ -131,6 +146,13 @@ const panelHeight = $derived(open ? height : MIN_HEIGHT);
aria-pressed={activeTab === tab.id && open}
>
{tab.label()}
+ {#if tab.id === 'discussion' && discussionCount > 0}
+
{discussionCount}
+ {/if}
{/each}
@@ -165,6 +187,7 @@ const panelHeight = $derived(open ? height : MIN_HEIGHT);
canComment={canComment}
currentUserId={currentUserId}
canAdmin={canAdmin}
+ onCountChange={handleCountChange}
/>
{:else if activeTab === 'history'}
diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte.spec.ts b/frontend/src/lib/components/DocumentBottomPanel.svelte.spec.ts
new file mode 100644
index 00000000..c36e8973
--- /dev/null
+++ b/frontend/src/lib/components/DocumentBottomPanel.svelte.spec.ts
@@ -0,0 +1,74 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page, userEvent } from 'vitest/browser';
+import DocumentBottomPanel from './DocumentBottomPanel.svelte';
+import type { Comment } from '$lib/types';
+
+afterEach(cleanup);
+
+function makeComment(id: string): Comment {
+ return {
+ id,
+ authorId: 'user-1',
+ authorName: 'Alice',
+ content: 'Hello',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ replies: []
+ };
+}
+
+const doc = { id: 'doc-1', title: 'Test' };
+
+const baseProps = {
+ doc,
+ canComment: true,
+ currentUserId: 'user-1',
+ canAdmin: false,
+ height: 300,
+ activeTab: 'discussion' as const
+};
+
+describe('DocumentBottomPanel – discussion badge', () => {
+ it('shows a badge with comment count on the Discussion tab when comments exist', async () => {
+ render(DocumentBottomPanel, {
+ ...baseProps,
+ comments: [makeComment('c-1'), makeComment('c-2')],
+ open: true
+ });
+ await expect.element(page.getByTestId('discussion-count-badge')).toBeInTheDocument();
+ await expect.element(page.getByTestId('discussion-count-badge')).toHaveTextContent('2');
+ });
+
+ it('does not show badge when there are no comments', async () => {
+ render(DocumentBottomPanel, { ...baseProps, comments: [], open: true });
+ await expect.element(page.getByTestId('discussion-count-badge')).not.toBeInTheDocument();
+ });
+});
+
+describe('DocumentBottomPanel – start discussion nudge', () => {
+ it('shows nudge above drag handle when panel is closed and no comments', async () => {
+ render(DocumentBottomPanel, { ...baseProps, comments: [], open: false });
+ await expect.element(page.getByTestId('discussion-nudge')).toBeInTheDocument();
+ });
+
+ it('does not show nudge when panel is closed but comments exist', async () => {
+ render(DocumentBottomPanel, {
+ ...baseProps,
+ comments: [makeComment('c-1')],
+ open: false
+ });
+ await expect.element(page.getByTestId('discussion-nudge')).not.toBeInTheDocument();
+ });
+
+ it('does not show nudge when panel is open', async () => {
+ render(DocumentBottomPanel, { ...baseProps, comments: [], open: true });
+ await expect.element(page.getByTestId('discussion-nudge')).not.toBeInTheDocument();
+ });
+
+ it('opens the discussion tab when the nudge is clicked', async () => {
+ render(DocumentBottomPanel, { ...baseProps, comments: [], open: false });
+ await userEvent.click(page.getByTestId('discussion-nudge'));
+ await expect.element(page.getByTestId('bottom-panel-content')).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/lib/components/PanelDiscussion.svelte b/frontend/src/lib/components/PanelDiscussion.svelte
index bbed758a..291cf5c1 100644
--- a/frontend/src/lib/components/PanelDiscussion.svelte
+++ b/frontend/src/lib/components/PanelDiscussion.svelte
@@ -8,9 +8,11 @@ type Props = {
canComment: boolean;
currentUserId: string | null;
canAdmin: boolean;
+ onCountChange?: (count: number) => void;
};
-let { documentId, initialComments, canComment, currentUserId, canAdmin }: Props = $props();
+let { documentId, initialComments, canComment, currentUserId, canAdmin, onCountChange }: Props =
+ $props();
@@ -20,5 +22,6 @@ let { documentId, initialComments, canComment, currentUserId, canAdmin }: Props
canComment={canComment}
currentUserId={currentUserId}
canAdmin={canAdmin}
+ onCountChange={onCountChange}
/>