diff --git a/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationClassificationGridItem/AnnotationClassificationGridItem.svelte b/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationClassificationGridItem/AnnotationClassificationGridItem.svelte
new file mode 100644
index 0000000000..1030e91097
--- /dev/null
+++ b/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationClassificationGridItem/AnnotationClassificationGridItem.svelte
@@ -0,0 +1,80 @@
+
+
+
+ {#if showLabel}
+
+
+ {/if}
+
diff --git a/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationClassificationGridItem/AnnotationClassificationGridItem.test.ts b/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationClassificationGridItem/AnnotationClassificationGridItem.test.ts
new file mode 100644
index 0000000000..020eafb5eb
--- /dev/null
+++ b/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationClassificationGridItem/AnnotationClassificationGridItem.test.ts
@@ -0,0 +1,152 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import '@testing-library/jest-dom';
+import { tick } from 'svelte';
+import {
+ SampleType,
+ AnnotationType,
+ type AnnotationWithPayloadView,
+ type ImageAnnotationView,
+ type VideoFrameAnnotationView,
+ type AnnotationView
+} from '$lib/api/lightly_studio_local';
+import AnnotationClassificationGridItem from './AnnotationClassificationGridItem.svelte';
+
+vi.mock('$lib/components/SampleClassificationPills/SampleClassificationPills.svelte', async () => {
+ const module = await import('./SampleClassificationPills.mock.svelte');
+ return { default: module.default };
+});
+
+vi.mock('$lib/utils', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ getGridImageURL: ({ sampleId }: { sampleId: string }) => `http://images/sample/${sampleId}`,
+ getGridFrameURL: ({ sampleId }: { sampleId: string }) => `http://frames/media/${sampleId}`
+ };
+});
+
+function buildAnnotation(
+ overrides: Partial = {}
+): AnnotationWithPayloadView {
+ return {
+ parent_sample_type: SampleType.IMAGE,
+ annotation: {
+ sample_id: 'ann-1',
+ annotation_type: AnnotationType.CLASSIFICATION,
+ annotation_label: { annotation_label_name: 'cat' },
+ annotation_collection_id: 'col-1',
+ parent_sample_id: 'img-1'
+ } as unknown as AnnotationView,
+ parent_sample_data: {
+ sample_id: 'img-1',
+ width: 800,
+ height: 600
+ } as unknown as ImageAnnotationView,
+ ...overrides
+ };
+}
+
+const defaultProps = {
+ containerWidth: 200,
+ containerHeight: 150,
+ showLabel: true,
+ cachedCollectionVersion: 'v1'
+};
+
+function renderItem(annotation: AnnotationWithPayloadView, props: Record = {}) {
+ return render(AnnotationClassificationGridItem, {
+ props: { annotation, ...defaultProps, ...props }
+ });
+}
+
+describe('AnnotationClassificationGridItem', () => {
+ it('renders thumbnail div and SampleClassificationPills for an image annotation', async () => {
+ const { container } = renderItem(buildAnnotation());
+
+ await tick();
+
+ const thumbnail = container.firstElementChild as HTMLElement;
+ expect(thumbnail).toBeInTheDocument();
+ expect(thumbnail.style.backgroundImage).toContain('img-1');
+ expect(screen.getByTestId('mock-classification-pills')).toBeInTheDocument();
+ });
+
+ it('renders thumbnail using frame URL for a video frame annotation', async () => {
+ const annotation = buildAnnotation({
+ parent_sample_type: SampleType.VIDEO_FRAME,
+ parent_sample_data: {
+ sample_id: 'frame-1',
+ video: { width: 1920, height: 1080, file_path_abs: '/video.mp4' }
+ } as unknown as VideoFrameAnnotationView
+ });
+ const { container } = renderItem(annotation);
+
+ await tick();
+
+ const thumbnail = container.firstElementChild as HTMLElement;
+ expect(thumbnail.style.backgroundImage).toContain('frames/media/frame-1');
+ });
+
+ it('applies grid-item-selected class when selected is true', () => {
+ const { container } = renderItem(buildAnnotation(), { selected: true });
+
+ expect(container.firstElementChild).toHaveAttribute('aria-selected', 'true');
+ });
+
+ it('hides SampleClassificationPills when showLabel is false', () => {
+ renderItem(buildAnnotation(), { showLabel: false });
+
+ expect(screen.queryByTestId('mock-classification-pills')).not.toBeInTheDocument();
+ });
+
+ it('passes only the single annotation to SampleClassificationPills', async () => {
+ renderItem(buildAnnotation());
+
+ await tick();
+
+ const pills = screen.getByTestId('mock-classification-pills');
+ // Exactly one annotation is passed — not all sibling labels for the parent sample.
+ expect(pills).toHaveAttribute('data-annotation-count', '1');
+ expect(pills).toHaveAttribute('data-annotation-id', 'ann-1');
+ });
+
+ it('calls onCropWindowChange with a full-image CropWindow (windowX/Y=0) once URL is available', async () => {
+ const onCropWindowChange = vi.fn();
+ const annotation = buildAnnotation({
+ parent_sample_data: {
+ sample_id: 'img-1',
+ width: 1024,
+ height: 768
+ } as unknown as ImageAnnotationView
+ });
+
+ renderItem(annotation, { onCropWindowChange });
+
+ await tick();
+
+ expect(onCropWindowChange).toHaveBeenCalledOnce();
+ const [annotationId, cropWindow] = onCropWindowChange.mock.calls[0];
+ expect(annotationId).toBe('ann-1');
+ expect(cropWindow.windowX).toBe(0);
+ expect(cropWindow.windowY).toBe(0);
+ expect(cropWindow.windowWidth).toBe(1024);
+ expect(cropWindow.windowHeight).toBe(768);
+ expect(cropWindow.sampleWidth).toBe(1024);
+ expect(cropWindow.sampleHeight).toBe(768);
+ });
+
+ it('calls onCropWindowChange with null on unmount', async () => {
+ const onCropWindowChange = vi.fn();
+
+ const { unmount } = renderItem(buildAnnotation(), { onCropWindowChange });
+
+ await tick();
+ unmount();
+ await tick();
+
+ const lastCall = onCropWindowChange.mock.calls.at(-1);
+ expect(lastCall?.[0]).toBe('ann-1');
+ expect(lastCall?.[1]).toBeNull();
+ });
+});
diff --git a/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationClassificationGridItem/SampleClassificationPills.mock.svelte b/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationClassificationGridItem/SampleClassificationPills.mock.svelte
new file mode 100644
index 0000000000..6ce63e4af1
--- /dev/null
+++ b/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationClassificationGridItem/SampleClassificationPills.mock.svelte
@@ -0,0 +1,15 @@
+
+
+
diff --git a/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationsGridItem/AnnotationClassificationGridItem.mock.svelte b/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationsGridItem/AnnotationClassificationGridItem.mock.svelte
new file mode 100644
index 0000000000..0f5c1815b0
--- /dev/null
+++ b/lightly_studio_view/src/lib/components/AnnotationsGrid/AnnotationsGridItem/AnnotationClassificationGridItem.mock.svelte
@@ -0,0 +1,16 @@
+
+
+
diff --git a/lightly_studio_view/src/lib/utils/index.ts b/lightly_studio_view/src/lib/utils/index.ts
index be465da7c1..3e3c2ef5b0 100644
--- a/lightly_studio_view/src/lib/utils/index.ts
+++ b/lightly_studio_view/src/lib/utils/index.ts
@@ -17,7 +17,8 @@ export { getImageURL as getImageURLById } from './getImageURL';
export {
getGridFrameURL,
getGridImageURL,
- getGridThumbnailRequestSize
+ getGridThumbnailRequestSize,
+ type GridThumbnailQuality
} from './getGridThumbnailURL/getGridThumbnailURL';
export { getVideoURLById } from './getVideoURLById/getVideoURLById';
export { getURL } from './getURL/getURL';