From 8975e4372980caf76921c4448408f82ac7e58235 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 21 Sep 2026 00:58:52 +0200 Subject: [PATCH] web-player: forms (CoverForm, SegmentedControl, Chip) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CoverForm.svelte is the shared skeleton (form + enhance + cover + error message + buttons) for AlbumForm and PlaylistForm, which now only keep their own fields via the `fields` snippet (next to the cover) and `extra` (full width — not literally in the spec, but needed: the album description and the playlist visibility picker don't fit in the column next to the art). ImageDropzone gains `size="hero"` (size-cover-hero + rounded-art-lg) and an `onselect` callback; the upload-track page now uses this component instead of reimplementing the cover picker by hand (wrapped in {#key} so it can reset after publishing). Its empty-state icon goes from 64px to icon-xl (36px), as the plan recommends. SegmentedControl.svelte (generic, N options) replaces PlaylistForm's Private/Public picker. Chip.svelte replaces Explore's filter chips (selected/count) and the suggested-genre chips in "No results" (href, outline style). @utility field-control in layout.css unifies the repeated class string shared by Input and Textarea. Verified with npm run check/build; no authenticated visual QA (Docker unavailable this session). --- web-player/docs/frontend-refactor-plan.md | 2 +- .../src/lib/components/ui/AlbumForm.svelte | 117 +++++-------- web-player/src/lib/components/ui/Chip.svelte | 36 ++++ .../src/lib/components/ui/CoverForm.svelte | 94 ++++++++++ .../lib/components/ui/ImageDropzone.svelte | 19 +- web-player/src/lib/components/ui/Input.svelte | 6 +- .../src/lib/components/ui/PlaylistForm.svelte | 163 ++++++------------ .../lib/components/ui/SegmentedControl.svelte | 34 ++++ .../src/lib/components/ui/Textarea.svelte | 3 +- .../src/routes/(app)/explore/+page.svelte | 24 +-- .../src/routes/(app)/upload/+page.svelte | 37 ++-- web-player/src/routes/layout.css | 3 + 12 files changed, 304 insertions(+), 234 deletions(-) create mode 100644 web-player/src/lib/components/ui/Chip.svelte create mode 100644 web-player/src/lib/components/ui/CoverForm.svelte create mode 100644 web-player/src/lib/components/ui/SegmentedControl.svelte diff --git a/web-player/docs/frontend-refactor-plan.md b/web-player/docs/frontend-refactor-plan.md index 0a4bc25..c000bff 100644 --- a/web-player/docs/frontend-refactor-plan.md +++ b/web-player/docs/frontend-refactor-plan.md @@ -783,7 +783,7 @@ Tareas: | P8 | ✅ | (pendiente) | Decisión tomada (recomendada por el propio plan, sin bloquear): se elimina la columna "Álbum" que siempre mostraba "—". TrackList unifica TrackTable+TrackRow+TrackIndexCell+TrackMeta; AlbumTrackTable/PlaylistTrackTable/LibraryPicker se inlinean en sus páginas y se borran. Sin QA visual autenticada (Docker no disponible) | | P9 | ✅ | (pendiente) | MediaCard gana hover-zoom del arte y scrim+PlayButton condicionados a `onPlay` (antes exclusivos de MixTile); ahora se aplicarían a cualquier card futura con onPlay, consistencia intencional. Sin QA visual autenticada (Docker no disponible) | | P10 | ✅ | (pendiente) | Decisión tomada (recomendada, sin bloquear): iconos en los botones de cabecera de playlist (antes solo álbum tenía). PlaylistHeader pierde su prop `id` sin usar tras inlinearlo. Explorar: la cabecera de "Resultados para…" ahora usa el `mb-7/8` de PageHeader en vez de un `mt-5` ajustado a mano antes de los chips de filtro — gap ligeramente mayor entre el resumen y los chips (Δ menor, documentado). Sin QA visual autenticada (Docker no disponible) | -| P11 | ⬜ | | | +| P11 | ✅ | (pendiente) | CoverForm añade snippet `extra` (no está en la spec literal, solo `fields`) para el contenido a ancho completo bajo la fila portada+campos (descripción de álbum, selector de visibilidad de playlist) que no cabía en una única columna. Upload usa ImageDropzone con `onselect` (prop nueva) y se envuelve en `{#key}` para poder resetear tras publicar — Δ: el contenedor de portada pasa de 144/176px (móvil/desktop) a 144px fijo (size-cover-hero, sin variante desktop mayor), y el icono de posición vacía de 64px a 36px (icon-xl), tal como recomienda el plan. Sin QA visual autenticada (Docker no disponible) | | P12 | ⬜ | | | | P13 | ⬜ | | | | P14 | ⬜ | | | diff --git a/web-player/src/lib/components/ui/AlbumForm.svelte b/web-player/src/lib/components/ui/AlbumForm.svelte index ba9280d..79f7691 100644 --- a/web-player/src/lib/components/ui/AlbumForm.svelte +++ b/web-player/src/lib/components/ui/AlbumForm.svelte @@ -1,13 +1,10 @@ -
{ - submitting = true; - return async ({ update, result }) => { - await update({ reset: false }); - submitting = false; - if (result.type === 'success') onSuccess?.(); - }; - }} - class="mt-5 w-full max-w-2xl space-y-5" + {coverFallbackUrl} + {requireCover} + coverIcon={Disc3} + {formMessage} + {submitLabel} + {submittingLabel} + canSubmit={title.trim() !== ''} + {onCancel} + {onSuccess} > -
- - + -
- - - - - - - -
-
- - - + class="min-h-0 field-control resize-none px-4 {klass}"> diff --git a/web-player/src/routes/(app)/explore/+page.svelte b/web-player/src/routes/(app)/explore/+page.svelte index adc4dfd..7cc6de5 100644 --- a/web-player/src/routes/(app)/explore/+page.svelte +++ b/web-player/src/routes/(app)/explore/+page.svelte @@ -13,6 +13,7 @@ import PlayButton from '$lib/components/ui/PlayButton.svelte'; import EqBars from '$lib/components/ui/EqBars.svelte'; import ListRow from '$lib/components/ui/ListRow.svelte'; + import Chip from '$lib/components/ui/Chip.svelte'; import SectionHeading from '$lib/components/ui/SectionHeading.svelte'; import { EXPLORE_PAGE_SIZE } from '$lib/config'; import { searchHref } from '$lib/navigation.svelte'; @@ -262,19 +263,13 @@
{#each searchChips as chip (chip.label)} - + {chip.label} + {/each}
@@ -361,12 +356,7 @@

{#each genreTiles.slice(0, 4) as genre (genre.query)} - - {genre.label} - + {genre.label} {/each}
diff --git a/web-player/src/routes/(app)/upload/+page.svelte b/web-player/src/routes/(app)/upload/+page.svelte index 3041a26..795cd8a 100644 --- a/web-player/src/routes/(app)/upload/+page.svelte +++ b/web-player/src/routes/(app)/upload/+page.svelte @@ -7,6 +7,7 @@ import Page from '$lib/components/ui/Page.svelte'; import PageHeader from '$lib/components/ui/PageHeader.svelte'; import Surface from '$lib/components/ui/Surface.svelte'; + import ImageDropzone from '$lib/components/ui/ImageDropzone.svelte'; import Alert from '$lib/components/ui/Alert.svelte'; import Field from '$lib/components/ui/Field.svelte'; import Input from '$lib/components/ui/Input.svelte'; @@ -21,7 +22,8 @@ let title = $state(''); let audioName = $state(''); let audioSize = $state(0); - let coverPreview = $state(''); + let hasCover = $state(false); + let coverResetToken = $state(0); let dragging = $state(false); let status = $state('idle'); let acceptedTerms = $state(false); @@ -38,7 +40,7 @@ const busy = $derived(status === 'uploading'); const canPublish = $derived( - title.trim() !== '' && audioName !== '' && coverPreview !== '' && acceptedTerms && !busy + title.trim() !== '' && audioName !== '' && hasCover && acceptedTerms && !busy ); const stepStates = $derived.by(() => { @@ -79,16 +81,12 @@ pickAudio(file); } - function onCoverInput(event: Event) { - const file = (event.currentTarget as HTMLInputElement).files?.[0]; - if (file) coverPreview = URL.createObjectURL(file); - } - function reset() { title = ''; audioName = ''; audioSize = 0; - coverPreview = ''; + hasCover = false; + coverResetToken += 1; status = 'idle'; acceptedTerms = false; errorMsg = ''; @@ -213,24 +211,15 @@
- + {/key} diff --git a/web-player/src/routes/layout.css b/web-player/src/routes/layout.css index 7dc4d29..d3fbc89 100644 --- a/web-player/src/routes/layout.css +++ b/web-player/src/routes/layout.css @@ -203,6 +203,9 @@ oklch(var(--mf-tile-shade-l) var(--mf-tile-shade-c) var(--tile-hue)) ); } +@utility field-control { + @apply w-full rounded-control bg-surface py-3 text-sm text-fg transition placeholder:text-muted focus:bg-surface-2 focus:outline-none; +} @layer base { svg.lucide {