feat(web): add ArtistRow component

One row in the artists list: avatar initial, name, album count,
chevron. Entire row is an <a> for click + keyboard activation.
This commit is contained in:
2026-04-23 18:18:52 -04:00
parent ce6b79ec95
commit a1252c1ef4
2 changed files with 52 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
<script lang="ts">
import type { ArtistRef } from '$lib/api/types';
let { artist }: { artist: ArtistRef } = $props();
const initial = $derived(artist.name ? artist.name.charAt(0).toUpperCase() : '');
const countLabel = $derived(artist.album_count === 1 ? '1 album' : `${artist.album_count} albums`);
</script>
<a
href={`/artists/${artist.id}`}
class="flex items-center gap-3 border-b border-border px-3 py-3 hover:bg-surface-hover focus-visible:ring-2 focus-visible:ring-accent"
>
<span
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface font-semibold"
aria-hidden="true"
>
{initial}
</span>
<span class="flex-1 truncate">{artist.name}</span>
<span class="text-sm text-text-secondary">{countLabel}</span>
<span class="text-text-secondary" aria-hidden="true"></span>
</a>
+29
View File
@@ -0,0 +1,29 @@
import { describe, expect, test } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import ArtistRow from './ArtistRow.svelte';
import type { ArtistRef } from '$lib/api/types';
const artist: ArtistRef = { id: 'abc', name: 'alice', album_count: 12 };
describe('ArtistRow', () => {
test('renders initial, name, album count inside a link to /artists/:id', () => {
render(ArtistRow, { props: { artist } });
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', '/artists/abc');
expect(link).toHaveTextContent('alice');
expect(link).toHaveTextContent('12 albums');
expect(link).toHaveTextContent('A'); // initial letter
});
test('singular "1 album" when album_count is 1', () => {
render(ArtistRow, { props: { artist: { ...artist, album_count: 1 } } });
expect(screen.getByRole('link')).toHaveTextContent('1 album');
});
test('empty name still renders a safe initial', () => {
render(ArtistRow, { props: { artist: { ...artist, name: '' } } });
// No crash; initial container present but empty or a placeholder char.
expect(screen.getByRole('link')).toBeInTheDocument();
});
});