diff --git a/web/src/lib/components/TrackRow.svelte b/web/src/lib/components/TrackRow.svelte
new file mode 100644
index 00000000..bd32ab62
--- /dev/null
+++ b/web/src/lib/components/TrackRow.svelte
@@ -0,0 +1,14 @@
+
+
+
+
+ {track.track_number ?? '—'}
+
+ {track.title}
+ {formatDuration(track.duration_sec)}
+
diff --git a/web/src/lib/components/TrackRow.test.ts b/web/src/lib/components/TrackRow.test.ts
new file mode 100644
index 00000000..1f108ea6
--- /dev/null
+++ b/web/src/lib/components/TrackRow.test.ts
@@ -0,0 +1,31 @@
+import { describe, expect, test } from 'vitest';
+import { render, screen } from '@testing-library/svelte';
+import TrackRow from './TrackRow.svelte';
+import type { TrackRef } from '$lib/api/types';
+
+const track: TrackRef = {
+ id: 't1',
+ title: 'So What',
+ album_id: 'xyz',
+ album_title: 'Kind of Blue',
+ artist_id: 'm-davis',
+ artist_name: 'Miles Davis',
+ track_number: 1,
+ disc_number: 1,
+ duration_sec: 545,
+ stream_url: '/api/tracks/t1/stream',
+};
+
+describe('TrackRow', () => {
+ test('renders track number, title, formatted duration', () => {
+ render(TrackRow, { props: { track } });
+ expect(screen.getByText('1')).toBeInTheDocument();
+ expect(screen.getByText('So What')).toBeInTheDocument();
+ expect(screen.getByText('9:05')).toBeInTheDocument();
+ });
+
+ test('track number falls back to dash when missing', () => {
+ render(TrackRow, { props: { track: { ...track, track_number: undefined } } });
+ expect(screen.getByText('—')).toBeInTheDocument();
+ });
+});