refactor(ui): add CollectionView component and unify track listing views

This commit is contained in:
2025-10-01 16:01:54 -04:00
parent dfdb236b2e
commit 56f909b243
4 changed files with 270 additions and 359 deletions

View File

@@ -0,0 +1,232 @@
<script lang="ts">
import type { Track } from '$lib/types/track';
import { convertFileSrc } from '@tauri-apps/api/core';
interface Props {
title: string;
subtitle?: string;
metadata?: string;
coverArtPath?: string;
tracks: Track[];
selectedTrackIndex?: number | null;
onTrackClick?: (index: number) => void;
showAlbumColumn?: boolean;
}
let {
title,
subtitle,
metadata,
coverArtPath,
tracks,
selectedTrackIndex = null,
onTrackClick,
showAlbumColumn = false
}: Props = $props();
function getThumbnailUrl(coverPath?: string): string {
if (!coverPath) {
return '';
}
return convertFileSrc(coverPath);
}
function handleTrackClick(index: number) {
if (onTrackClick) {
onTrackClick(index);
}
}
</script>
<!-- Header -->
<div class="collection-header">
{#if coverArtPath}
<img
src={getThumbnailUrl(coverArtPath)}
alt="{title} cover"
class="collection-cover"
/>
{:else}
<div class="collection-cover-placeholder"></div>
{/if}
<div class="collection-info">
<h2>{title}</h2>
{#if subtitle}
<p class="collection-subtitle">{subtitle}</p>
{/if}
{#if metadata}
<p class="collection-metadata">{metadata}</p>
{/if}
</div>
</div>
<section class="collection-content">
<!-- Tabs (single tab for tracks) -->
<!--
svelte-ignore a11y_no_noninteractive_element_to_interactive_role
Reason: 98.css library requires <menu role="tablist"> for proper tab styling.
The role="tablist" selector is used by 98.css CSS rules (menu[role="tablist"]).
The <menu> element IS interactive (contains clickable <button> elements) and the
role="tablist" properly describes the semantic purpose to assistive technology.
This is the documented pattern from 98.css and matches WAI-ARIA tab widget patterns.
-->
<menu role="tablist">
<li role="tab" aria-selected={true}>
<button>Tracks</button>
</li>
</menu>
<!-- Track Listing -->
<div class="window tab-content" role="tabpanel">
<div class="window-body">
<div class="sunken-panel table-container">
<table class="interactive">
<thead>
<tr>
<th style="width: 50px;">#</th>
<th>Title</th>
{#if showAlbumColumn}
<th>Artist</th>
<th>Album</th>
{/if}
<th>Duration</th>
<th>Format</th>
</tr>
</thead>
<tbody>
{#each tracks as track, i}
<tr
class:highlighted={selectedTrackIndex === i}
onclick={() => handleTrackClick(i)}
>
<td class="track-number">
{track.metadata.trackNumber ?? i + 1}
</td>
<td>{track.metadata.title || track.filename}</td>
{#if showAlbumColumn}
<td>{track.metadata.artist || '—'}</td>
<td>{track.metadata.album || '—'}</td>
{/if}
<td class="duration">
{#if track.metadata.duration}
{Math.floor(track.metadata.duration / 60)}:{String(Math.floor(track.metadata.duration % 60)).padStart(2, '0')}
{:else}
{/if}
</td>
<td class="format">{track.format.toUpperCase()}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
</section>
<style>
.collection-header {
display: flex;
gap: 16px;
padding: 8px;
margin-bottom: 6px;
flex-shrink: 0;
}
.collection-cover {
width: 152px;
height: 152px;
object-fit: cover;
image-rendering: auto;
flex-shrink: 0;
}
.collection-cover-placeholder {
width: 152px;
height: 152px;
background: linear-gradient(135deg, #c0c0c0 25%, #808080 25%, #808080 50%, #c0c0c0 50%, #c0c0c0 75%, #808080 75%);
background-size: 8px 8px;
flex-shrink: 0;
}
.collection-info {
display: flex;
flex-direction: column;
justify-content: center;
}
h2 {
margin: 0 0 4px 0;
font-size: 1.5em;
}
.collection-subtitle {
margin: 0 0 8px 0;
font-size: 1.1em;
opacity: 0.8;
}
.collection-metadata {
margin: 0;
opacity: 0.6;
font-size: 0.9em;
}
.collection-content {
margin: 0;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.tab-content {
margin-top: -2px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.window-body {
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.table-container {
flex: 1;
overflow-y: auto;
min-height: 0;
}
table {
width: 100%;
}
th {
text-align: left;
}
.track-number {
text-align: center;
opacity: 0.6;
}
.duration {
font-family: monospace;
font-size: 0.9em;
text-align: center;
width: 80px;
}
.format {
font-family: monospace;
font-size: 0.85em;
text-transform: uppercase;
text-align: center;
width: 80px;
}
</style>