refactor: tighten the home typography and card craft

The playlist cards were showing "No description was pr…" because the API
stored that English sentence whenever a playlist was created without a
description. Descriptions are optional now and the old rows are cleared.

Section headings drop to 18/20px so they stop competing with the content,
and the space moved from inside the sections to between them.

The three media cards repeated the same anchor, art class and label
block, so they share a MediaCard that also carries the hover lift the
track tiles already had.
This commit is contained in:
2026-07-21 23:14:36 +02:00
parent b4b9e886e8
commit e728b333a9
26 changed files with 1317 additions and 88 deletions
@@ -22,7 +22,7 @@
<header
class="sticky top-0 z-40 flex h-14 items-center justify-between border-b border-line bg-bg/85 page-x backdrop-blur-md md:hidden"
>
<a href="/" class="text-lg font-extrabold tracking-wide">Musify</a>
<a href="/" class="text-lg font-semibold tracking-wide">Musify</a>
<div class="relative" bind:this={menuRef}>
<button
@@ -1,4 +1,5 @@
<script lang="ts">
import MediaCard from './MediaCard.svelte';
import PlaylistArt from './PlaylistArt.svelte';
import Cover from './Cover.svelte';
import { hueFor } from '$lib/theme/color';
@@ -45,30 +46,25 @@
);
</script>
<a
<MediaCard
href={href ?? `/albums/${id}`}
class="group animate-enter min-w-0 text-left"
style="animation-delay:{index * 45}ms"
{title}
subtitle={meta}
{index}
oncontextmenu={onContextMenu}
>
{#if coverSrc}
<Cover
trackId={id}
src={coverSrc}
hue={hueFor(id)}
size="large"
alt={title}
class="aspect-square w-full rounded-art shadow-art transition group-hover:shadow-art-lg"
/>
{:else}
<PlaylistArt
{trackIds}
hue={hueFor(id)}
class="aspect-square w-full rounded-art shadow-art transition group-hover:shadow-art-lg"
/>
{/if}
<div class="mt-2.5 truncate text-fg">{title}</div>
{#if meta}
<div class="mt-0.5 truncate text-sm text-fg-3">{meta}</div>
{/if}
</a>
{#snippet art(artClass)}
{#if coverSrc}
<Cover
trackId={id}
src={coverSrc}
hue={hueFor(id)}
size="large"
alt={title}
class={artClass}
/>
{:else}
<PlaylistArt {trackIds} hue={hueFor(id)} class={artClass} />
{/if}
{/snippet}
</MediaCard>
@@ -20,7 +20,7 @@
</span>
{/if}
{#if title}
<h3 class="mt-5 text-2xl font-bold tracking-tight text-fg">{title}</h3>
<h3 class="mt-5 text-2xl font-semibold tracking-tight text-fg">{title}</h3>
{/if}
{#if description}
<p class="mt-2 text-fg-2">{description}</p>
@@ -0,0 +1,32 @@
<script lang="ts" module>
export const mediaCardArt =
'aspect-square w-full rounded-art shadow-art transition duration-300 ease-out group-hover:-translate-y-1 group-hover:shadow-art-lg';
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
href: string;
title: string;
subtitle?: string | null;
index?: number;
oncontextmenu?: (event: MouseEvent) => void;
art: Snippet<[string]>;
}
let { href, title, subtitle, index = 0, oncontextmenu, art }: Props = $props();
</script>
<a
{href}
class="group animate-enter min-w-0 text-left"
style="animation-delay:{index * 45}ms"
{oncontextmenu}
>
{@render art(mediaCardArt)}
<div class="mt-2.5 truncate text-fg transition-colors group-hover:text-accent-soft">{title}</div>
{#if subtitle}
<div class="mt-0.5 truncate text-sm text-fg-3">{subtitle}</div>
{/if}
</a>
@@ -11,7 +11,7 @@
let {
as = 'div',
min = '200px',
min = '165px',
minMobile = '150px',
class: klass = '',
children
@@ -1,4 +1,5 @@
<script lang="ts">
import MediaCard from './MediaCard.svelte';
import MixArt from './MixArt.svelte';
import { hueFor } from '$lib/theme/color';
import type { Mix } from '$lib/mixes';
@@ -13,19 +14,13 @@
const count = $derived(Number(mix.itemCount));
</script>
<a
<MediaCard
href="/mixes/{mix.id}"
class="group animate-enter min-w-0 text-left"
style="animation-delay:{index * 45}ms"
title={mix.title}
subtitle="{count} {count === 1 ? 'canción' : 'canciones'}"
{index}
>
<MixArt
items={mix.items}
hue={hueFor(mix.id)}
class="aspect-square w-full rounded-art shadow-art transition group-hover:shadow-art-lg"
/>
<div class="mt-2.5 truncate text-fg">{mix.title}</div>
<div class="mt-0.5 truncate text-sm text-fg-3">
{count}
{count === 1 ? 'canción' : 'canciones'}
</div>
</a>
{#snippet art(artClass)}
<MixArt items={mix.items} hue={hueFor(mix.id)} class={artClass} />
{/snippet}
</MediaCard>
@@ -82,7 +82,7 @@
>
{#if title}
<div class="flex items-center justify-between">
<h2 class="text-lg font-bold tracking-tight text-fg">{title}</h2>
<h2 class="text-lg font-semibold tracking-tight text-fg">{title}</h2>
<IconButton label="Cerrar" onclick={onClose}>
<X class="h-4 w-4" />
</IconButton>
@@ -13,7 +13,7 @@
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between {klass}">
<div class="min-w-0">
<h1 class="text-2xl font-bold tracking-tight text-fg sm:text-4xl">{title}</h1>
<h1 class="text-2xl font-semibold tracking-tight text-fg sm:text-4xl">{title}</h1>
{#if subtitle}
<p class="mt-2 text-sm text-fg-2 sm:text-base">{subtitle}</p>
{/if}
@@ -1,4 +1,5 @@
<script lang="ts">
import MediaCard from './MediaCard.svelte';
import PlaylistArt from './PlaylistArt.svelte';
import { hueFor } from '$lib/theme/color';
@@ -14,20 +15,8 @@
let { id, name, description, trackIds, updatedAt, index = 0 }: Props = $props();
</script>
<a
href="/playlists/{id}"
class="group animate-enter min-w-0 text-left"
style="animation-delay:{index * 45}ms"
>
<PlaylistArt
playlistId={id}
{trackIds}
hue={hueFor(id)}
version={updatedAt}
class="aspect-square w-full rounded-art shadow-art transition group-hover:shadow-art-lg"
/>
<div class="mt-2.5 truncate text-fg">{name}</div>
{#if description}
<div class="mt-0.5 truncate text-sm text-fg-3">{description}</div>
{/if}
</a>
<MediaCard href="/playlists/{id}" title={name} subtitle={description} {index}>
{#snippet art(artClass)}
<PlaylistArt playlistId={id} {trackIds} hue={hueFor(id)} version={updatedAt} class={artClass} />
{/snippet}
</MediaCard>
@@ -11,10 +11,12 @@
</script>
{#if actions}
<div class="mb-4 flex items-center justify-between gap-4 sm:mb-6 {klass}">
<h2 class="text-xl font-bold text-fg sm:text-2xl">{title}</h2>
<div class="mb-3 flex items-baseline justify-between gap-4 sm:mb-4 {klass}">
<h2 class="text-lg font-semibold tracking-tight text-fg sm:text-xl">{title}</h2>
{@render actions()}
</div>
{:else}
<h2 class="mb-4 text-xl font-bold text-fg sm:mb-6 sm:text-2xl {klass}">{title}</h2>
<h2 class="mb-3 text-lg font-semibold tracking-tight text-fg sm:mb-4 sm:text-xl {klass}">
{title}
</h2>
{/if}