feat(web): add DiscoverResultCard with reserved badge slot + anchored button
T14 of the M5a Lidarr plan. The Discover grid in M5a renders mixed
requestable / kept / requested cards from Lidarr search. Without
layout discipline the cards in a row land their titles at different
Y coordinates whenever the badge presence varies, which reads as
visual noise. DiscoverResultCard enforces:
- Flex column with `margin-top: auto` on `.actions`, so the action
button is anchored to the bottom of the card body regardless of
title/subtitle wrap differences.
- `.badge-row` always rendered with `min-height: 22px`, so the
title baseline holds even when no Kept pill is present.
Both load-bearing CSS values use inline style attributes — jsdom's
getComputedStyle does not resolve scoped <style> blocks, so the tests
verify positioning via inline styles directly. The remaining decorative
CSS (kept-pill background = accent at 15%, flex-direction, gap) lives
in a scoped <style> block.
State -> action mapping (sentence case per FabledSword voice):
requestable -> bg-action-primary "Request" with Plus icon
kept -> disabled ghost "In library" + Kept pill
requested -> disabled ghost "Requested"
Cover art falls back to a Lucide glyph (Disc3 / Album / Music2) when
imageUrl is absent. Adds lucide-svelte@^1.0.1 dependency.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<script lang="ts" module>
|
||||
export type DiscoverCardKind = 'artist' | 'album' | 'track';
|
||||
export type DiscoverCardState = 'requestable' | 'kept' | 'requested';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Plus, Disc3, Album, Music2 } from 'lucide-svelte';
|
||||
|
||||
let {
|
||||
kind,
|
||||
title,
|
||||
subtitle,
|
||||
imageUrl,
|
||||
state,
|
||||
onRequest,
|
||||
}: {
|
||||
kind: DiscoverCardKind;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
imageUrl?: string;
|
||||
state: DiscoverCardState;
|
||||
onRequest?: () => void;
|
||||
} = $props();
|
||||
|
||||
const FallbackIcon = $derived(
|
||||
kind === 'artist' ? Disc3 : kind === 'album' ? Album : Music2
|
||||
);
|
||||
|
||||
function handleRequest() {
|
||||
if (state === 'requestable') onRequest?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
<article
|
||||
class="card rounded-lg border border-border bg-surface p-3"
|
||||
data-state={state}
|
||||
>
|
||||
<div
|
||||
class="art flex aspect-square w-full items-center justify-center overflow-hidden rounded-md bg-surface-hover"
|
||||
>
|
||||
{#if imageUrl}
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<FallbackIcon size={32} strokeWidth={1} class="text-text-muted" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="text mt-3">
|
||||
<div class="title text-base font-medium text-text-primary">{title}</div>
|
||||
{#if subtitle}
|
||||
<div class="subtitle text-sm text-text-secondary">{subtitle}</div>
|
||||
{/if}
|
||||
<div
|
||||
class="badge-row"
|
||||
data-testid="badge-row"
|
||||
style="min-height: 22px;"
|
||||
>
|
||||
{#if state === 'kept'}
|
||||
<span class="kept-pill">Kept</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions pt-3" data-testid="actions" style="margin-top: auto;">
|
||||
{#if state === 'requestable'}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 rounded-md bg-action-primary px-3 py-1.5 text-sm text-text-primary"
|
||||
onclick={handleRequest}
|
||||
>
|
||||
<Plus size={16} strokeWidth={1} /> Request
|
||||
</button>
|
||||
{:else if state === 'kept'}
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
onclick={handleRequest}
|
||||
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-muted"
|
||||
>
|
||||
In library
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
onclick={handleRequest}
|
||||
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-muted"
|
||||
>
|
||||
Requested
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.badge-row {
|
||||
min-height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.actions {
|
||||
margin-top: auto;
|
||||
}
|
||||
.kept-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
background: color-mix(in srgb, var(--fs-accent) 15%, transparent);
|
||||
color: var(--fs-accent);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user