Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
25 changed files with 326 additions and 359 deletions
Showing only changes of commit 58eb670fed - Show all commits
+1 -1
View File
@@ -775,7 +775,7 @@ Tareas:
| P0 | ✅ | (pendiente) | Button primary/secondary se mantienen iguales (decisión del usuario) |
| P1 | ✅ | (pendiente) | Verificado con `build` (Tailwind compila `@apply sm:` dentro de `@utility`); sin QA visual autenticada (Docker no disponible en esta sesión) |
| P2 | ✅ | (pendiente) | `<style>{expr}</style>` no interpola en Svelte (se trata como CSS del componente); solución: `{@html \`<style>${css}</style>\`}` |
| P3 | ⬜ | | |
| P3 | ✅ | (pendiente) | Se quitó el nombre bajo el avatar de perfil (decisión tomada: redundante con el título de CollectionHeader). Se añadió `size="fill"` a Artwork (no está en la spec) para las tarjetas de grid fluidas (AlbumCard/PlaylistCard/MixTile), ya que P9 (MediaCard) aún no existe. SearchResultRow perdió su prop `kind`/radio-por-tipo (ahora el radio lo decide Artwork/Avatar). Sin QA visual autenticada (Docker no disponible) |
| P4 | ⬜ | | |
| P5 | ⬜ | | |
| P6 | ⬜ | | |
@@ -20,7 +20,7 @@
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import ListPlus from '@lucide/svelte/icons/list-plus';
import ListEnd from '@lucide/svelte/icons/list-end';
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte';
import GlassMenu from '$lib/components/ui/GlassMenu.svelte';
interface Props {
@@ -99,11 +99,10 @@
type="submit"
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg transition hover:bg-hover"
>
<PlaylistArt
playlistId={playlist.id}
trackIds={[]}
size="small"
class="h-9 w-9 shrink-0 rounded-control"
<Artwork
src="/api/playlists/{playlist.id}/cover?size=small"
size="xs"
class="shrink-0"
/>
<span class="truncate text-fg-2">{playlist.name}</span>
</button>
@@ -5,6 +5,7 @@
import LogIn from '@lucide/svelte/icons/log-in';
import MenuItem from './ui/MenuItem.svelte';
import AccountMenu from './ui/AccountMenu.svelte';
import Avatar from './ui/Avatar.svelte';
interface Props {
user: SessionUser | null;
@@ -40,15 +41,7 @@
aria-expanded={open}
class="flex items-center rounded-full"
>
{#if user.picture}
<img src={user.picture} alt="" class="h-9 w-9 rounded-full object-cover" />
{:else}
<span
class="grid h-9 w-9 place-items-center rounded-full bg-surface-2 text-sm uppercase"
>
{user.name.charAt(0)}
</span>
{/if}
<Avatar name={user.name} src={user.picture} size="sm" />
</button>
{/snippet}
{#snippet extraItems({ close })}
+7 -6
View File
@@ -4,7 +4,7 @@
import { liked } from '$lib/player/liked.svelte';
import { createPlaylistModal } from '$lib/playlists.svelte';
import type { SessionUser } from '$lib/types';
import PlaylistArt from './ui/PlaylistArt.svelte';
import Artwork from './ui/Artwork.svelte';
import Home from '@lucide/svelte/icons/house';
import Compass from '@lucide/svelte/icons/compass';
import ListMusic from '@lucide/svelte/icons/list-music';
@@ -97,12 +97,13 @@
? 'bg-surface-hover'
: 'hover:bg-hover'}"
>
<PlaylistArt
playlistId={playlist.id}
<Artwork
src="/api/playlists/{playlist.id}/cover?size=small&v={encodeURIComponent(
playlist.updatedAt
)}"
trackIds={playlist.coverTrackIds}
version={playlist.updatedAt}
size="small"
class="h-8.5 w-8.5 shrink-0 overflow-hidden rounded-lg opacity-90"
size="xs"
class="shrink-0 opacity-90"
/>
<div class="min-w-0 flex-1">
<div class="truncate text-sm {active ? 'text-fg' : 'text-fg-2'}">
+8 -10
View File
@@ -8,6 +8,7 @@
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import AccountMenu from './ui/AccountMenu.svelte';
import Avatar from './ui/Avatar.svelte';
interface Props {
user: SessionUser | null;
@@ -113,17 +114,14 @@
onclick={toggle}
aria-label="Tu cuenta"
aria-expanded={open}
class="block h-8.25 w-8.25 shrink-0 overflow-hidden rounded-full border border-line-strong transition active:scale-95"
class="block shrink-0 rounded-full transition active:scale-95"
>
{#if user.picture}
<img src={user.picture} alt="" class="h-full w-full object-cover opacity-90" />
{:else}
<span
class="grid h-full w-full place-items-center bg-surface-2 text-xs font-semibold text-fg uppercase"
>
{user.name.charAt(0)}
</span>
{/if}
<Avatar
name={user.name}
src={user.picture}
size="sm"
class="border border-line-strong opacity-90"
/>
</button>
{/snippet}
</AccountMenu>
@@ -21,7 +21,7 @@
import { enhance } from '$app/forms';
import ListPlus from '@lucide/svelte/icons/list-plus';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte';
import GlassMenu from '$lib/components/ui/GlassMenu.svelte';
interface Props {
@@ -91,11 +91,10 @@
type="submit"
class="flex w-full items-center gap-3 rounded-control px-2 py-2.5 text-left text-sm font-medium text-fg transition hover:bg-hover"
>
<PlaylistArt
playlistId={playlist.id}
trackIds={[]}
size="small"
class="h-9 w-9 shrink-0 rounded-control"
<Artwork
src="/api/playlists/{playlist.id}/cover?size=small"
size="xs"
class="shrink-0"
/>
<span class="truncate text-fg-2">{playlist.name}</span>
</button>
@@ -1,7 +1,7 @@
<script lang="ts">
import { player } from '$lib/player/player.svelte';
import { queuePanel } from '$lib/player/queuePanel.svelte';
import Cover from '$lib/components/ui/Cover.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte';
import ArtistLink from '$lib/components/ui/ArtistLink.svelte';
import { fmtTime } from '$lib/format';
import X from '@lucide/svelte/icons/x';
@@ -35,11 +35,11 @@
{#if player.current.id}
<p class="mb-2.75 text-xs font-medium tracking-widest text-fg-3 uppercase">Reproduciendo</p>
<div class="mb-6 flex items-center gap-3 rounded-xl bg-accent-tint p-2.5">
<Cover
trackId={player.current.id}
size="small"
<Artwork
trackIds={[player.current.id]}
size="sm"
alt={player.current.title}
class="h-10 w-10 shrink-0 rounded-lg"
class="shrink-0"
/>
<div class="min-w-0">
<div class="truncate text-sm text-accent-soft">
@@ -55,9 +55,7 @@
{/if}
<div class="mb-2.75 flex items-center justify-between gap-3">
<span
class="min-w-0 flex-1 truncate text-xs font-medium tracking-widest text-fg-3 uppercase"
>
<span class="min-w-0 flex-1 truncate text-xs font-medium tracking-widest text-fg-3 uppercase">
A continuación
</span>
{#if upcoming.length > 0}
@@ -85,12 +83,7 @@
}}
class="flex items-center gap-3 rounded-control p-2.25 text-left hover:bg-hover"
>
<Cover
trackId={track.id}
size="small"
alt={track.title}
class="h-10 w-10 shrink-0 rounded-lg opacity-80"
/>
<Artwork trackIds={[track.id]} size="sm" alt={track.title} class="shrink-0 opacity-80" />
<div class="min-w-0 flex-1">
<div class="truncate text-xs text-fg">{track.title}</div>
<ArtistLink
@@ -1,7 +1,7 @@
<script lang="ts">
import { player } from '$lib/player/player.svelte';
import { liked } from '$lib/player/liked.svelte';
import Cover from '$lib/components/ui/Cover.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte';
import ExplicitBadge from '$lib/components/ui/ExplicitBadge.svelte';
import ArtistLink from '$lib/components/ui/ArtistLink.svelte';
import Heart from '@lucide/svelte/icons/heart';
@@ -29,14 +29,14 @@
<div class="flex min-w-0 items-center gap-2.5 md:gap-3.25">
{#if player.current.id}
<Cover
trackId={player.current.id}
size="small"
<Artwork
trackIds={[player.current.id]}
size="sm"
alt={player.current.title}
class="h-11 w-11 shrink-0 rounded-xl md:h-12 md:w-12"
class="shrink-0 md:size-cover-md"
/>
{:else}
<div class="h-11 w-11 shrink-0 rounded-xl bg-surface md:h-12 md:w-12"></div>
<div class="size-cover-sm shrink-0 rounded-art bg-surface md:size-cover-md"></div>
{/if}
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-1.5">
@@ -1,7 +1,6 @@
<script lang="ts">
import MediaCard from './MediaCard.svelte';
import PlaylistArt from './PlaylistArt.svelte';
import Cover from './Cover.svelte';
import Artwork from './Artwork.svelte';
interface Props {
id: string;
@@ -53,10 +52,6 @@
oncontextmenu={onContextMenu}
>
{#snippet art(artClass)}
{#if coverSrc}
<Cover trackId={id} src={coverSrc} size="large" alt={title} class={artClass} />
{:else}
<PlaylistArt {trackIds} class={artClass} />
{/if}
<Artwork src={coverSrc} {trackIds} size="fill" alt={title} class={artClass} />
{/snippet}
</MediaCard>
@@ -1,36 +0,0 @@
<script lang="ts">
interface Props {
name: string;
imageUrl?: string | null;
size?: number;
}
let { name, imageUrl, size = 108 }: Props = $props();
const initials = $derived(
name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase() ?? '')
.join('')
);
</script>
<div class="flex flex-col items-center gap-2.5 text-center" style="width:{size}px">
<div
class="relative grid shrink-0 place-items-center overflow-hidden rounded-full bg-surface ring-1 ring-line transition duration-300 ease-out ring-inset"
style="width:{size}px;height:{size}px"
>
{#if imageUrl}
<img src={imageUrl} alt="" class="absolute inset-0 h-full w-full object-cover" />
{:else}
<div
class="pointer-events-none absolute inset-0"
style="background:repeating-linear-gradient(135deg, color-mix(in oklch, var(--mf-text) 12%, transparent) 0 2px, transparent 2px 10px)"
></div>
<span class="relative text-lg font-semibold text-fg">{initials}</span>
{/if}
</div>
<div class="w-full truncate text-sm text-fg">{name}</div>
</div>
@@ -0,0 +1,133 @@
<script lang="ts">
import type { Snippet } from 'svelte';
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'hero' | 'fill';
interface Props {
src?: string | null;
trackIds?: (string | number)[];
size: Size;
shape?: 'auto' | 'round';
imageSize?: 'small' | 'medium' | 'large';
fallback?: 'music' | 'gradient' | 'none';
alt?: string;
class?: string;
children?: Snippet;
}
let {
src,
trackIds = [],
size,
shape = 'auto',
imageSize,
fallback = 'music',
alt = '',
class: klass = '',
children
}: Props = $props();
const DIMENSION: Record<Size, string> = {
xs: 'size-cover-xs',
sm: 'size-cover-sm',
md: 'size-cover-md',
lg: 'size-cover-lg',
xl: 'size-cover-xl',
'2xl': 'size-cover-2xl',
hero: 'size-cover-hero sm:size-cover-hero-sm',
fill: 'aspect-square w-full'
};
const RADIUS: Record<Size, string> = {
xs: 'rounded-thumb',
sm: 'rounded-art',
md: 'rounded-art',
lg: 'rounded-art',
xl: 'rounded-art',
'2xl': 'rounded-art-lg',
hero: 'rounded-art-lg',
fill: ''
};
const AUTO_IMAGE_SIZE: Record<Size, 'small' | 'medium' | 'large'> = {
xs: 'small',
sm: 'small',
md: 'medium',
lg: 'medium',
xl: 'medium',
'2xl': 'large',
hero: 'large',
fill: 'large'
};
const resolvedImageSize = $derived(imageSize ?? AUTO_IMAGE_SIZE[size]);
const radius = $derived(shape === 'round' ? 'rounded-full' : RADIUS[size]);
let srcFailed = $state(false);
$effect(() => {
src;
srcFailed = false;
});
let singleFailed = $state(false);
$effect(() => {
trackIds[0];
singleFailed = false;
});
const showSrc = $derived(!!src && !srcFailed);
const showMosaic = $derived(!showSrc && trackIds.length >= 4);
const singleId = $derived(!showSrc && !showMosaic && trackIds.length > 0 ? trackIds[0] : null);
const showSingle = $derived(singleId !== null && !singleFailed);
</script>
<div class="relative overflow-hidden {DIMENSION[size]} {radius} {klass}">
{#if showSrc}
<img
{src}
{alt}
loading="lazy"
decoding="async"
fetchpriority="low"
onerror={() => (srcFailed = true)}
class="h-full w-full object-cover"
/>
{:else if showMosaic}
<div class="grid h-full w-full grid-cols-2 grid-rows-2">
{#each trackIds.slice(0, 4) as id (id)}
<img
src="/api/tracks/{id}/cover?size=small"
alt=""
loading="lazy"
decoding="async"
fetchpriority="low"
onerror={(e) => ((e.currentTarget as HTMLImageElement).style.visibility = 'hidden')}
class="h-full w-full object-cover"
/>
{/each}
</div>
{:else if showSingle}
<img
src="/api/tracks/{singleId}/cover?size={resolvedImageSize}"
{alt}
loading="lazy"
decoding="async"
fetchpriority="low"
onerror={() => (singleFailed = true)}
class="h-full w-full object-cover"
/>
{:else if fallback === 'music'}
<div class="grid h-full w-full place-items-center bg-surface">
<svg viewBox="0 0 24 24" fill="currentColor" class="h-2/5 w-2/5 text-fg-3">
<path
d="M19.952 1.651a.75.75 0 0 1 .298.599V16.303a3 3 0 0 1-2.176 2.884l-1.32.377a2.553 2.553 0 1 1-1.401-4.909l2.311-.66a1.5 1.5 0 0 0 1.088-1.442V6.994l-9 2.572v9.737a3 3 0 0 1-2.176 2.884l-1.32.377a2.553 2.553 0 1 1-1.402-4.909l2.312-.66a1.5 1.5 0 0 0 1.088-1.442V5.25a.75.75 0 0 1 .544-.721l10.5-3a.75.75 0 0 1 .754.122Z"
/>
</svg>
</div>
{:else if fallback === 'gradient'}
<div class="h-full w-full bg-[image:var(--mf-cover-grad)]"></div>
{:else}
<div class="h-full w-full bg-surface"></div>
{/if}
{@render children?.()}
</div>
@@ -0,0 +1,60 @@
<script lang="ts">
type Size = 'sm' | 'md' | 'lg' | 'hero';
interface Props {
name: string;
src?: string | null;
size: Size;
class?: string;
}
let { name, src, size, class: klass = '' }: Props = $props();
const DIMENSION: Record<Size, string> = {
sm: 'size-cover-xs',
md: 'size-cover-lg',
lg: 'size-cover-xl',
hero: 'size-cover-hero'
};
const TEXT: Record<Size, string> = {
sm: 'text-sm',
md: 'text-base',
lg: 'text-xl',
hero: 'text-3xl'
};
const initials = $derived(
name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase() ?? '')
.join('')
);
let failed = $state(false);
$effect(() => {
src;
failed = false;
});
</script>
<div
class="relative grid shrink-0 place-items-center overflow-hidden rounded-full bg-surface-2 {DIMENSION[
size
]} {klass}"
>
{#if src && !failed}
<img
{src}
alt=""
loading="lazy"
decoding="async"
onerror={() => (failed = true)}
class="absolute inset-0 h-full w-full object-cover"
/>
{:else}
<span class="font-semibold text-fg uppercase {TEXT[size]}">{initials}</span>
{/if}
</div>
@@ -1,47 +0,0 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
trackId: string | number;
size?: 'small' | 'medium' | 'large';
src?: string | null;
alt?: string;
class?: string;
children?: Snippet;
oncontextmenu?: (event: MouseEvent) => void;
}
let {
trackId,
size = 'medium',
src,
alt = '',
class: klass = '',
children,
oncontextmenu
}: Props = $props();
let failed = $state(false);
const imageSrc = $derived(src ?? `/api/tracks/${trackId}/cover?size=${size}`);
$effect(() => {
imageSrc;
failed = false;
});
</script>
<div class="relative overflow-hidden bg-surface {klass}" role="presentation" {oncontextmenu}>
{#if !failed}
<img
src={imageSrc}
{alt}
loading="lazy"
decoding="async"
fetchpriority="low"
onerror={() => (failed = true)}
class="h-full w-full object-cover"
/>
{/if}
{@render children?.()}
</div>
@@ -1,25 +0,0 @@
<script lang="ts">
import Cover from './Cover.svelte';
import { mixItemKey, type MixItem } from '$lib/mixes';
interface Props {
items: MixItem[];
class?: string;
}
let { items, class: klass = '' }: Props = $props();
const tiles = $derived(items.slice(0, 4));
</script>
{#if tiles.length >= 4}
<div class="grid grid-cols-2 grid-rows-2 overflow-hidden {klass}">
{#each tiles as item (mixItemKey(item))}
<Cover trackId={item.trackId} size="small" class="h-full w-full" />
{/each}
</div>
{:else if tiles.length > 0}
<Cover trackId={tiles[0].trackId} size="medium" class={klass} />
{:else}
<div class="bg-surface {klass}"></div>
{/if}
+13 -10
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import Play from '@lucide/svelte/icons/play';
import MixArt from './MixArt.svelte';
import Artwork from './Artwork.svelte';
import { player } from '$lib/player/player.svelte';
import { queueItemForTarget } from '$lib/tracks';
import { mixItemTarget, type Mix } from '$lib/mixes';
@@ -13,6 +13,8 @@
let { mix, index = 0, class: klass = '' }: Props = $props();
const trackIds = $derived(mix.items.map((item) => item.trackId));
function play(event: MouseEvent) {
event.preventDefault();
const items = mix.items.map((item) => queueItemForTarget(mixItemTarget(item)));
@@ -23,15 +25,16 @@
<a
href="/mixes/{mix.id}"
class="group/tile animate-enter block cursor-pointer {klass}"
style="animation-delay:{Math.min(index, 10) * 45}ms"
style="--i:{index}"
>
<Artwork
{trackIds}
size="fill"
alt={mix.title}
class="rounded-art transition duration-300 group-hover/tile:scale-105"
>
<div class="relative aspect-square overflow-hidden rounded-art">
<MixArt
items={mix.items}
class="h-full w-full scale-100 transition duration-300 group-hover/tile:scale-105"
/>
<div
class="pointer-events-none absolute inset-0 bg-linear-to-b from-transparent from-45% to-black/42"
class="pointer-events-none absolute inset-0 bg-linear-to-b from-transparent from-45% to-scrim/40"
></div>
<button
type="button"
@@ -41,8 +44,8 @@
>
<Play class="h-3.5 w-3.5" fill="currentColor" />
</button>
</div>
<div class="mt-3.25 truncate text-sm font-semibold tracking-[-0.01em] text-fg">
</Artwork>
<div class="mt-3.25 truncate text-sm font-semibold text-fg">
{mix.title}
</div>
{#if mix.subtitle}
@@ -1,61 +0,0 @@
<script lang="ts">
import Cover from './Cover.svelte';
interface Props {
playlistId?: string;
coverUrl?: string;
trackIds: (string | number)[];
size?: 'small' | 'medium' | 'large';
version?: string;
class?: string;
}
let {
playlistId,
coverUrl,
trackIds,
size = 'medium',
version,
class: klass = ''
}: Props = $props();
let coverFailed = $state(false);
const mosaic = $derived(trackIds.length >= 4);
const single = $derived(trackIds.length >= 1 && trackIds.length < 4 ? trackIds[0] : null);
const src = $derived(
coverUrl
? coverUrl
: playlistId
? `/api/playlists/${playlistId}/cover?size=${size}${version ? `&v=${encodeURIComponent(version)}` : ''}`
: undefined
);
</script>
{#if src && !coverFailed}
<div class="relative overflow-hidden bg-surface {klass}">
<img
{src}
alt=""
loading="lazy"
onerror={() => (coverFailed = true)}
class="h-full w-full object-cover"
/>
</div>
{:else if mosaic}
<div class="grid grid-cols-2 grid-rows-2 overflow-hidden {klass}">
{#each trackIds.slice(0, 4) as id (id)}
<Cover trackId={id} size="small" class="h-full w-full" />
{/each}
</div>
{:else if single !== null}
<Cover trackId={single} size="medium" class={klass} />
{:else}
<div class="flex items-center justify-center bg-surface {klass}">
<svg viewBox="0 0 24 24" fill="currentColor" class="h-2/5 w-2/5 text-fg-3">
<path
d="M19.952 1.651a.75.75 0 0 1 .298.599V16.303a3 3 0 0 1-2.176 2.884l-1.32.377a2.553 2.553 0 1 1-1.401-4.909l2.311-.66a1.5 1.5 0 0 0 1.088-1.442V6.994l-9 2.572v9.737a3 3 0 0 1-2.176 2.884l-1.32.377a2.553 2.553 0 1 1-1.402-4.909l2.312-.66a1.5 1.5 0 0 0 1.088-1.442V5.25a.75.75 0 0 1 .544-.721l10.5-3a.75.75 0 0 1 .754.122Z"
/>
</svg>
</div>
{/if}
@@ -1,6 +1,6 @@
<script lang="ts">
import MediaCard from './MediaCard.svelte';
import PlaylistArt from './PlaylistArt.svelte';
import Artwork from './Artwork.svelte';
interface Props {
id: string;
@@ -18,6 +18,12 @@
<MediaCard href="/playlists/{id}" title={name} {subtitle} {index}>
{#snippet art(artClass)}
<PlaylistArt playlistId={id} {trackIds} version={updatedAt} class={artClass} />
<Artwork
src="/api/playlists/{id}/cover?size=large&v={encodeURIComponent(updatedAt)}"
{trackIds}
size="fill"
alt={name}
class={artClass}
/>
{/snippet}
</MediaCard>
@@ -1,20 +1,8 @@
<script lang="ts" module>
export type SearchResultKind = 'track' | 'album' | 'playlist' | 'user';
</script>
<script lang="ts">
import type { Snippet } from 'svelte';
import ExplicitBadge from './ExplicitBadge.svelte';
const RADIUS: Record<SearchResultKind, string> = {
track: 'rounded-control',
album: 'rounded-md',
playlist: 'rounded-art',
user: 'rounded-full'
};
interface Props {
kind: SearchResultKind;
title: string;
subtitle?: string | null;
meta?: string;
@@ -26,7 +14,6 @@
}
let {
kind,
title,
subtitle,
meta,
@@ -37,7 +24,7 @@
art
}: Props = $props();
const artClass = $derived(`relative h-13 w-13 shrink-0 overflow-hidden ${RADIUS[kind]}`);
const artClass = 'relative shrink-0';
</script>
{#snippet content()}
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import Cover from './Cover.svelte';
import Artwork from './Artwork.svelte';
import ExplicitBadge from './ExplicitBadge.svelte';
import ArtistLink from './ArtistLink.svelte';
@@ -10,7 +10,7 @@
artist?: string | null;
ownerUserId?: string | number | null;
coverSrc?: string | null;
coverSize?: string;
size?: 'sm' | 'lg';
explicit?: boolean;
active?: boolean;
titleClass?: string;
@@ -25,7 +25,7 @@
artist,
ownerUserId,
coverSrc,
coverSize = 'h-10.5 w-10.5',
size = 'sm',
explicit = false,
active = false,
titleClass = '',
@@ -53,15 +53,9 @@
onkeydown={onKeydown}
class="flex min-w-0 items-center gap-3.5 text-left"
>
<Cover
{trackId}
src={coverSrc}
size="small"
alt={title}
class="{coverSize} shrink-0 rounded-control"
>
<Artwork trackIds={[trackId]} src={coverSrc} {size} alt={title} class="shrink-0">
{@render overlay?.()}
</Cover>
</Artwork>
<div class="min-w-0">
<div class="flex min-w-0 items-center gap-2">
{#if explicit}
@@ -3,7 +3,7 @@
import Pause from '@lucide/svelte/icons/pause';
import SquarePencil from '@lucide/svelte/icons/square-pen';
import Trash from '@lucide/svelte/icons/trash';
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte';
import BackLink from '$lib/components/ui/BackLink.svelte';
import Button from '$lib/components/ui/Button.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
@@ -51,12 +51,7 @@
<CollectionHeader eyebrow="Álbum" {title} {description} {meta}>
{#snippet cover()}
<PlaylistArt
{trackIds}
{coverUrl}
size="large"
class="h-36 w-36 shrink-0 rounded-art-lg sm:h-41 sm:w-41"
/>
<Artwork src={coverUrl} {trackIds} size="hero" alt={title} class="shrink-0" />
{/snippet}
{#snippet actions()}
<Button size="sm" onclick={onPlayAll} disabled={trackIds.length === 0}>
@@ -9,8 +9,8 @@
import Alert from '$lib/components/ui/Alert.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte';
import InfiniteScroll from '$lib/components/ui/InfiniteScroll.svelte';
import Cover from '$lib/components/ui/Cover.svelte';
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte';
import Avatar from '$lib/components/ui/Avatar.svelte';
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
import SearchResultRow from '$lib/components/ui/SearchResultRow.svelte';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
@@ -296,40 +296,25 @@
{#if topResult}
{#snippet topResultBody()}
{#if topResult.kind === 'track'}
<Cover
trackId={targetId(topResult.target)}
size="large"
<Artwork
trackIds={[targetId(topResult.target)]}
size="xl"
alt={targetTitle(topResult.target)}
class="h-22 w-22 shrink-0 rounded-2xl"
class="shrink-0"
/>
{:else if topResult.kind === 'album'}
<PlaylistArt
trackIds={topResult.album.coverTrackIds}
class="h-22 w-22 shrink-0 rounded-2xl"
/>
<Artwork trackIds={topResult.album.coverTrackIds} size="xl" class="shrink-0" />
{:else if topResult.kind === 'playlist'}
<PlaylistArt
playlistId={topResult.playlist.id}
<Artwork
src="/api/playlists/{topResult.playlist.id}/cover?size=large&v={encodeURIComponent(
topResult.playlist.updatedAt
)}"
trackIds={topResult.playlist.coverTrackIds}
version={topResult.playlist.updatedAt}
class="h-22 w-22 shrink-0 rounded-2xl"
size="xl"
class="shrink-0"
/>
{:else}
<div
class="grid h-22 w-22 shrink-0 place-items-center overflow-hidden rounded-full bg-surface-2"
>
{#if topResult.user.profilePictureUrl}
<img
src={topResult.user.profilePictureUrl}
alt=""
class="h-full w-full object-cover"
/>
{:else}
<span class="text-xl font-semibold text-fg uppercase"
>{topResult.user.name.charAt(0)}</span
>
{/if}
</div>
<Avatar name={topResult.user.name} src={topResult.user.profilePictureUrl} size="lg" />
{/if}
<div class="flex h-22 min-w-0 flex-1 flex-col justify-center gap-3">
@@ -443,7 +428,6 @@
{#each capped(songRows) as { target: item, seconds }, i (targetId(item))}
<li>
<SearchResultRow
kind="track"
title={targetTitle(item)}
subtitle={targetArtist(item)}
meta="{fmtTime(seconds)} · {fmtPlays(targetListensCount(item))}"
@@ -452,16 +436,16 @@
oncontextmenu={(e) => openContextMenu(e, item)}
>
{#snippet art(artClass)}
<Cover
trackId={targetId(item)}
size="small"
<Artwork
trackIds={[targetId(item)]}
size="lg"
alt={targetTitle(item)}
class="{artClass} ring-1 ring-line ring-inset"
>
{#if isTargetCurrent(item)}
<NowPlaying paused={!player.playing} />
{/if}
</Cover>
</Artwork>
{/snippet}
</SearchResultRow>
</li>
@@ -495,14 +479,13 @@
{#each capped(albums) as album (album.id)}
<li>
<SearchResultRow
kind="album"
title={album.title}
meta="{Number(album.trackCount)} canciones"
href="/albums/{album.id}"
oncontextmenu={(e) => openAlbumMenu(e, album.id)}
>
{#snippet art(artClass)}
<PlaylistArt trackIds={album.coverTrackIds} class={artClass} />
<Artwork trackIds={album.coverTrackIds} size="lg" class={artClass} />
{/snippet}
</SearchResultRow>
</li>
@@ -525,17 +508,9 @@
<ul class="flex flex-col gap-1">
{#each capped(users) as u (u.id)}
<li>
<SearchResultRow kind="user" title={u.name} href="/u/{u.id}">
<SearchResultRow title={u.name} href="/u/{u.id}">
{#snippet art(artClass)}
<div class="{artClass} grid place-items-center bg-surface-2">
{#if u.profilePictureUrl}
<img src={u.profilePictureUrl} alt="" class="h-full w-full object-cover" />
{:else}
<span class="text-sm font-semibold text-fg uppercase">
{u.name.charAt(0)}
</span>
{/if}
</div>
<Avatar name={u.name} src={u.profilePictureUrl} size="md" class={artClass} />
{/snippet}
</SearchResultRow>
</li>
@@ -559,16 +534,17 @@
{#each capped(playlistMatches) as playlist (playlist.id)}
<li>
<SearchResultRow
kind="playlist"
title={playlist.name}
subtitle={playlist.description}
href="/playlists/{playlist.id}"
>
{#snippet art(artClass)}
<PlaylistArt
playlistId={playlist.id}
<Artwork
src="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
playlist.updatedAt
)}"
trackIds={playlist.coverTrackIds}
version={playlist.updatedAt}
size="lg"
class={artClass}
/>
{/snippet}
@@ -9,7 +9,7 @@
import BackLink from '$lib/components/ui/BackLink.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Alert from '$lib/components/ui/Alert.svelte';
import MixArt from '$lib/components/ui/MixArt.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
import TrackTable from '$lib/components/ui/TrackTable.svelte';
import TrackRow from '$lib/components/ui/TrackRow.svelte';
@@ -75,7 +75,12 @@
meta="{items.length} {items.length === 1 ? 'canción' : 'canciones'} · {fmtTime(totalSeconds)}"
>
{#snippet cover()}
<MixArt {items} class="h-36 w-36 shrink-0 rounded-art-lg sm:h-41 sm:w-41" />
<Artwork
trackIds={items.map((item) => item.trackId)}
size="hero"
alt={mix.title}
class="shrink-0"
/>
{/snippet}
{#snippet actions()}
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
@@ -1,5 +1,5 @@
<script lang="ts">
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte';
import Button from '$lib/components/ui/Button.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
@@ -44,12 +44,12 @@
align="center"
>
{#snippet cover()}
<PlaylistArt
playlistId={id}
<Artwork
src="/api/playlists/{id}/cover?size=large&v={encodeURIComponent(updatedAt)}"
{trackIds}
size="large"
version={updatedAt}
class="h-36 w-36 shrink-0 rounded-art-lg sm:h-41 sm:w-41"
size="hero"
alt={name}
class="shrink-0"
/>
{/snippet}
{#snippet actions()}
@@ -7,7 +7,7 @@
import PlaylistCard from '$lib/components/ui/PlaylistCard.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte';
import Button from '$lib/components/ui/Button.svelte';
import ArtistAvatar from '$lib/components/ui/ArtistAvatar.svelte';
import Avatar from '$lib/components/ui/Avatar.svelte';
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
@@ -65,7 +65,7 @@
actions={canFollow ? followAction : undefined}
>
{#snippet cover()}
<ArtistAvatar name={profile.name} imageUrl={profile.profilePictureUrl} size={144} />
<Avatar name={profile.name} src={profile.profilePictureUrl} size="hero" />
{/snippet}
</CollectionHeader>
+20 -21
View File
@@ -20,7 +20,7 @@
type TrackTarget
} from '$lib/tracks';
import ArtistLink from '$lib/components/ui/ArtistLink.svelte';
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
import Artwork from '$lib/components/ui/Artwork.svelte';
import MixTile from '$lib/components/ui/MixTile.svelte';
import EqBars from '$lib/components/ui/EqBars.svelte';
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte';
@@ -215,7 +215,7 @@
title={item.title}
artist={item.artist}
ownerUserId={item.ownerUserId}
coverSize="h-14 w-14"
size="lg"
active={isCurrent}
onClick={() => playContinue(i)}
/>
@@ -250,12 +250,14 @@
onclick={() => goto(`/playlists/${spotlight.id}`)}
onkeydown={(e) => onRowKeydown(e, () => goto(`/playlists/${spotlight.id}`))}
>
<PlaylistArt
playlistId={spotlight.id}
<Artwork
src="/api/playlists/{spotlight.id}/cover?size=large&v={encodeURIComponent(
spotlight.updatedAt
)}"
trackIds={spotlight.coverTrackIds}
version={spotlight.updatedAt}
size="large"
class="h-34 w-34 shrink-0 rounded-2xl"
size="2xl"
alt={spotlight.name}
class="shrink-0"
/>
<div class="flex min-w-0 flex-col gap-3">
<p class="text-xs font-medium tracking-widest text-fg-2 uppercase">
@@ -292,9 +294,7 @@
? 'bg-accent-tint'
: 'hover:bg-hover'}"
>
<span class="w-5 shrink-0 text-center text-xs text-muted tabular-nums"
>{i + 1}</span
>
<span class="w-5 shrink-0 text-center text-xs text-muted tabular-nums">{i + 1}</span>
<div class="min-w-0 flex-1">
<div
class="truncate text-sm {isTargetCurrent(target)
@@ -309,9 +309,7 @@
class="mt-0.5 text-xs text-fg-3"
/>
</div>
<span class="shrink-0 text-xs text-muted tabular-nums"
>{fmtTime(seconds)}</span
>
<span class="shrink-0 text-xs text-muted tabular-nums">{fmtTime(seconds)}</span>
</div>
{:else}
<p class="p-2 text-sm text-fg-3">Esta playlist todavía no tiene canciones.</p>
@@ -369,7 +367,6 @@
title={targetTitle(target)}
artist={targetArtist(target)}
ownerUserId={targetOwnerUserId(target)}
coverSize="h-10 w-10"
explicit={targetExplicit(target)}
active={isCurrent}
onClick={() => playPopular(i)}
@@ -414,17 +411,19 @@
<div
role="button"
tabindex="0"
class="group/card animate-enter flex items-center gap-3.5 rounded-2xl p-3 transition-colors hover:bg-hover"
style="animation-delay:{Math.min(i, 10) * 40}ms"
class="group/card animate-enter flex items-center gap-3.5 rounded-panel p-3 transition-colors hover:bg-hover"
style="--i:{i}"
onclick={() => goto(`/playlists/${playlist.id}`)}
onkeydown={(e) => onRowKeydown(e, () => goto(`/playlists/${playlist.id}`))}
>
<PlaylistArt
playlistId={playlist.id}
<Artwork
src="/api/playlists/{playlist.id}/cover?size=medium&v={encodeURIComponent(
playlist.updatedAt
)}"
trackIds={playlist.coverTrackIds}
version={playlist.updatedAt}
size="medium"
class="h-15.5 w-15.5 shrink-0 rounded-control"
size="lg"
alt={playlist.name}
class="shrink-0"
/>
<div class="min-w-0 flex-1">
<div class="truncate text-sm tracking-tight text-fg">