Extract shared data grid, selection bar, file dropzone and genre picker components

Track lists and the upload queue now render through a declarative DataGrid, the album editor reuses AlbumForm with a split layout and a track picker, and genre labels and types are shared helpers.
This commit is contained in:
2026-09-25 01:17:59 +02:00
parent 797bed8e7d
commit 074ea7bf26
26 changed files with 879 additions and 663 deletions
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import LibraryAlbumEditor from '$lib/components/music/library/LibraryAlbumEditor.svelte';
let { data, form } = $props();
@@ -9,6 +10,7 @@
{#key data.album.id}
<LibraryAlbumEditor
album={data.album}
artist={page.data.user?.name ?? ''}
tracks={data.tracks.items}
memberIds={data.memberIds}
formMessage={form?.message}
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import LibraryAlbumEditor from '$lib/components/music/library/LibraryAlbumEditor.svelte';
let { data, form } = $props();
@@ -8,6 +9,7 @@
<div class="mt-4">
<LibraryAlbumEditor
album={null}
artist={page.data.user?.name ?? ''}
tracks={data.tracks.items}
memberIds={[]}
formMessage={form?.action === 'saveAlbum' ? form.message : undefined}
@@ -14,10 +14,9 @@
import Checkbox from '$lib/components/ui/primitives/Checkbox.svelte';
import Button from '$lib/components/ui/primitives/Button.svelte';
import ExplicitBadge from '$lib/components/ui/media/ExplicitBadge.svelte';
import Chip from '$lib/components/ui/primitives/Chip.svelte';
import { LIMITS } from '$lib/validation';
import { formatSize } from '$lib/utils/format';
import { genreOptions } from '$lib/data/genres';
import GenrePicker from '$lib/components/music/GenrePicker.svelte';
type Status = 'idle' | 'uploading' | 'done' | 'error';
type StepState = 'idle' | 'active' | 'done';
@@ -44,15 +43,6 @@
{ key: 'done', label: 'Listo', hint: 'Se procesa y aparece en tu biblioteca.' }
];
const options = $derived(genreOptions(data.genres));
const blocked = $derived(
new Set(
options
.filter((option) => tags.includes(option.genre))
.flatMap((option) => option.incompatibleWith)
)
);
const busy = $derived(status === 'uploading');
const canPublish = $derived(
@@ -91,10 +81,6 @@
pickAudio(file);
}
function toggleTag(tag: string) {
tags = tags.includes(tag) ? tags.filter((current) => current !== tag) : [...tags, tag];
}
function reset() {
title = '';
audioName = '';
@@ -258,28 +244,12 @@
{/snippet}
</Field>
<Field label="Géneros">
<div class="mb-2 flex flex-wrap gap-2">
{#each options as option (option.genre)}
<Chip
selected={tags.includes(option.genre)}
disabled={blocked.has(option.genre)}
onclick={() => toggleTag(option.genre)}
>
{option.label}
</Chip>
{/each}
</div>
{#each tags as tag (tag)}
<input type="hidden" name="tags" value={tag} />
{/each}
{#snippet hint()}
<p>
Elige al menos uno. Algunos géneros no se pueden combinar, como Metal con Ambient.
</p>
<span class="shrink-0 tabular-nums">{tags.length} elegidos</span>
{/snippet}
</Field>
<GenrePicker
genres={data.genres}
bind:selected={tags}
name="tags"
help="Elige al menos uno. Algunos géneros no se pueden combinar, como Metal con Ambient."
/>
<Checkbox bind:checked={isExplicit} name="isExplicit">
<span class="inline-flex items-center gap-2">