feat(frontend): assemble accessible application shell

This commit is contained in:
Justin Visser 2026-08-10 15:04:17 +02:00
parent 96c69507ec
commit 036ef3cc6f
27 changed files with 1852 additions and 13 deletions

View file

@ -0,0 +1,169 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { TrackEvent } from '../lib/types'
const props = defineProps<{ event: TrackEvent }>()
const artworkFailed = ref(false)
const artists = computed(() => props.event.track.artists.join(', '))
</script>
<template>
<article class="card">
<div class="rank">{{ event.rank }}</div>
<div class="art">
<img
v-if="event.track.album_art_url && !artworkFailed"
:src="event.track.album_art_url"
:alt="`${event.track.album_name} album artwork`"
@error="artworkFailed = true"
/>
</div>
<div class="track-meta">
<div class="head">
<span class="title">{{ event.track.title }}</span>
<span class="artist">{{ artists }}</span>
</div>
<div class="album">{{ event.track.album_name }}</div>
<div class="why">{{ event.justification }}</div>
</div>
<a
v-if="event.track.external_url"
class="link"
:href="event.track.external_url"
target="_blank"
rel="noreferrer"
>
Open in Spotify
<span aria-hidden="true">&nearr;</span>
</a>
</article>
</template>
<style scoped>
.card {
display: grid;
grid-template-columns: 26px 52px 1fr auto;
row-gap: var(--s-4);
column-gap: var(--s-6);
align-items: center;
padding: 11px var(--s-5);
background: var(--c-surface);
border: 1px solid var(--c-card-border);
border-radius: var(--r-md);
transition:
border-color var(--dur-fast) ease,
background var(--dur-fast) ease;
animation: card-in var(--dur-card) var(--ease) both;
}
.card:hover {
background: var(--c-surface-hover);
border-color: var(--c-line-strong);
}
.rank {
color: var(--c-text-faint);
font-family: var(--f-mono);
font-size: 12px;
text-align: right;
}
.art {
width: 52px;
height: 52px;
overflow: hidden;
background: repeating-linear-gradient(135deg, var(--c-art-a) 0 6px, var(--c-art-b) 6px 12px);
border: 1px solid var(--c-line);
border-radius: var(--r-sm);
}
.art img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.track-meta {
min-width: 0;
}
.head {
display: flex;
flex-wrap: wrap;
gap: 0 9px;
align-items: baseline;
}
.title {
font-size: var(--t-title);
font-weight: 500;
}
.artist,
.album,
.why {
color: var(--c-text-muted);
font-size: 13px;
}
.album {
margin-top: 1px;
color: var(--c-text-faint);
font-family: var(--f-mono);
font-size: var(--t-micro);
}
.why {
margin-top: 3px;
color: var(--c-text-dim);
font-size: var(--t-small);
text-wrap: pretty;
}
.link {
display: inline-flex;
gap: var(--s-1);
align-items: center;
justify-content: center;
justify-self: end;
padding: 6px var(--s-4);
color: var(--c-text-muted);
font-family: var(--f-mono);
font-size: var(--t-micro);
text-decoration: none;
white-space: nowrap;
border: 1px solid var(--c-line);
border-radius: var(--r-md);
}
.link:hover {
color: var(--c-accent);
text-decoration: none;
border-color: var(--c-accent);
}
@keyframes card-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: none;
}
}
@media (max-width: 560px) {
.card {
grid-template-columns: 26px 52px 1fr;
}
.link {
grid-column: 1 / -1;
justify-self: stretch;
min-height: var(--tap-min);
}
}
</style>