Animate library tabs, rows, editor track lists, status badges and dropzone
Tabs get a sliding indicator that persists across library routes, grid rows flip and collapse on removal, the selection bar and filters crossfade, album tracks move between lists with a crossfade, processing badges pulse and the dropzone scales while dragging.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import type { LibraryTrack } from '$lib/types';
|
||||
import { plural } from '$lib/utils/format';
|
||||
import { transferBetweenLists } from '$lib/utils/transitions';
|
||||
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
||||
import TrackPickList from './TrackPickList.svelte';
|
||||
|
||||
@@ -14,6 +15,8 @@
|
||||
|
||||
let { tracks, memberIds }: Props = $props();
|
||||
|
||||
const [send, receive] = transferBetweenLists();
|
||||
|
||||
let currentIds = $state<string[]>(untrack(() => [...memberIds]));
|
||||
|
||||
const initialIds = untrack(() => new Set(memberIds));
|
||||
@@ -43,13 +46,13 @@
|
||||
|
||||
{#if available.length > 0}
|
||||
<h3 class="mt-6 mb-2 px-3 text-eyebrow text-fg-2">Añadir desde tu biblioteca</h3>
|
||||
<TrackPickList tracks={available} icon={Plus} onPick={add} />
|
||||
<TrackPickList tracks={available} icon={Plus} {send} {receive} onPick={add} />
|
||||
{/if}
|
||||
|
||||
<div class="mt-6">
|
||||
{#if inAlbum.length > 0}
|
||||
<h3 class="mb-2 px-3 text-eyebrow text-fg-2">En este álbum</h3>
|
||||
<TrackPickList tracks={inAlbum} icon={X} onPick={remove} />
|
||||
<TrackPickList tracks={inAlbum} icon={X} {send} {receive} onPick={remove} />
|
||||
{:else}
|
||||
<EmptyState
|
||||
title="Este álbum aún no tiene canciones"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<script lang="ts" module>
|
||||
const FADE = { duration: 200 };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import Upload from '@lucide/svelte/icons/upload';
|
||||
import Music from '@lucide/svelte/icons/music';
|
||||
import SearchX from '@lucide/svelte/icons/search-x';
|
||||
@@ -64,25 +69,32 @@
|
||||
{/if}
|
||||
|
||||
{#if tracks.length > 0}
|
||||
{#if selectedIds.length > 0}
|
||||
<SelectionBar
|
||||
class="mt-4"
|
||||
label={plural(selectedIds.length, 'seleccionada', 'seleccionadas')}
|
||||
onClear={() => (selectedIds = [])}
|
||||
>
|
||||
<Button size="sm" variant="danger" onclick={deleteSelected}>Borrar</Button>
|
||||
</SelectionBar>
|
||||
{:else}
|
||||
<div class="mt-4 flex min-h-11 flex-wrap items-center gap-x-4 gap-y-3">
|
||||
<SearchInput
|
||||
bind:value={query}
|
||||
placeholder="Filtrar por título o artista"
|
||||
size="sm"
|
||||
class="w-full sm:max-w-search sm:flex-1"
|
||||
/>
|
||||
<ChipGroup label="Filtrar por estado" options={filterOptions} bind:value={filter} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-4 grid">
|
||||
{#if selectedIds.length > 0}
|
||||
<div class="col-start-1 row-start-1" in:fade={FADE} out:fade={FADE}>
|
||||
<SelectionBar
|
||||
label={plural(selectedIds.length, 'seleccionada', 'seleccionadas')}
|
||||
onClear={() => (selectedIds = [])}
|
||||
>
|
||||
<Button size="sm" variant="danger" onclick={deleteSelected}>Borrar</Button>
|
||||
</SelectionBar>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="col-start-1 row-start-1 flex min-h-11 flex-wrap items-center gap-x-4 gap-y-3"
|
||||
in:fade={FADE}
|
||||
out:fade={FADE}
|
||||
>
|
||||
<SearchInput
|
||||
bind:value={query}
|
||||
placeholder="Filtrar por título o artista"
|
||||
size="sm"
|
||||
class="w-full sm:max-w-search sm:flex-1"
|
||||
/>
|
||||
<ChipGroup label="Filtrar por estado" options={filterOptions} bind:value={filter} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if visible.length > 0}
|
||||
<ManageTrackList
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
columns={COLUMNS}
|
||||
rows={tracks}
|
||||
rowKey={(track) => track.id}
|
||||
stagger
|
||||
rowClass={(track) => (player.currentId === track.id ? 'surface-active' : 'hover:bg-hover')}
|
||||
>
|
||||
{#snippet head()}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type { LucideIcon } from '@lucide/svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { expoOut } from 'svelte/easing';
|
||||
import type { LibraryTrack } from '$lib/types';
|
||||
import type { transferBetweenLists } from '$lib/utils/transitions';
|
||||
import ListRow from '$lib/components/ui/media/ListRow.svelte';
|
||||
|
||||
type Transfer = ReturnType<typeof transferBetweenLists>;
|
||||
|
||||
interface Props {
|
||||
tracks: LibraryTrack[];
|
||||
icon: LucideIcon;
|
||||
send: Transfer[0];
|
||||
receive: Transfer[1];
|
||||
onPick: (track: LibraryTrack) => void;
|
||||
}
|
||||
|
||||
let { tracks, icon: Icon, onPick }: Props = $props();
|
||||
let { tracks, icon: Icon, send, receive, onPick }: Props = $props();
|
||||
|
||||
function subtitle(track: LibraryTrack): string {
|
||||
const album = track.albumTitle ? `en ${track.albumTitle}` : 'Sin álbum';
|
||||
@@ -19,7 +26,12 @@
|
||||
|
||||
<div class="flex flex-col gap-px" role="list">
|
||||
{#each tracks as track (track.id)}
|
||||
<div role="listitem">
|
||||
<div
|
||||
role="listitem"
|
||||
animate:flip={{ duration: 200, easing: expoOut }}
|
||||
in:receive|global={{ key: track.id }}
|
||||
out:send|global={{ key: track.id }}
|
||||
>
|
||||
<ListRow
|
||||
size="xs"
|
||||
trackId={track.id}
|
||||
|
||||
Reference in New Issue
Block a user