web-player: P11 — formularios (CoverForm, SegmentedControl, Chip)

CoverForm.svelte es el esqueleto compartido (form + enhance + portada
+ mensaje de error + botones) de AlbumForm y PlaylistForm, que quedan
solo con sus campos propios vía snippets `fields` (junto a la
portada) y `extra` (a ancho completo, no está en la spec literal pero
necesario: la descripción del álbum y el selector de visibilidad de
playlist no caben en la columna junto al arte).

ImageDropzone gana `size="hero"` (size-cover-hero + rounded-art-lg) y
un callback `onselect`; la página de subir canción usa ahora este
componente en vez de reimplementar el selector de portada a mano
(envuelto en {#key} para poder resetear tras publicar). Su icono de
posición vacía pasa de 64px a icon-xl (36px), como recomienda el plan.

SegmentedControl.svelte (genérico, N opciones) sustituye el selector
Privada/Pública de PlaylistForm. Chip.svelte sustituye los chips de
filtro de Explorar (selected/count) y los chips de género sugeridos en
"Sin resultados" (href, estilo outline).

@utility field-control en layout.css unifica la cadena de clases
repetida de Input y Textarea.

Verificado con npm run check/build; sin QA visual autenticada (Docker
no disponible en esta sesión).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 00:58:52 +02:00
co-authored by Claude Sonnet 5
parent 2cf3e9f02d
commit 90f9a76628
12 changed files with 304 additions and 234 deletions
@@ -8,6 +8,8 @@
required?: boolean;
icon?: LucideIcon;
gradient?: boolean;
size?: 'default' | 'hero';
onselect?: (file: File) => void;
class?: string;
}
@@ -17,9 +19,16 @@
required = false,
icon: Icon = ImageIcon,
gradient = false,
class: klass = 'h-40 w-40 sm:h-52 sm:w-52 rounded-control'
size = 'default',
onselect,
class: klass = ''
}: Props = $props();
const SIZE: Record<'default' | 'hero', string> = {
default: 'h-40 w-40 sm:h-52 sm:w-52 rounded-control',
hero: 'size-cover-hero rounded-art-lg'
};
let preview = $state('');
let fallbackFailed = $state(false);
@@ -27,14 +36,16 @@
function onInput(event: Event) {
const file = (event.currentTarget as HTMLInputElement).files?.[0];
if (file) preview = URL.createObjectURL(file);
if (!file) return;
preview = URL.createObjectURL(file);
onselect?.(file);
}
</script>
<label
class="relative grid cursor-pointer place-items-center overflow-hidden transition-colors {gradient
? ''
: 'border border-line bg-surface hover:border-accent/50'} {klass}"
: 'border border-line bg-surface hover:border-accent/50'} {SIZE[size]} {klass}"
style={gradient && !hasImage ? 'background:var(--mf-cover-grad)' : undefined}
>
<input
@@ -57,6 +68,6 @@
class="h-full w-full object-cover"
/>
{:else}
<Icon class="h-9 w-9 {gradient ? 'text-on-art/70' : 'text-muted'}" strokeWidth={1.3} />
<Icon class="size-icon-xl {gradient ? 'text-on-art/70' : 'text-muted'}" strokeWidth={1.3} />
{/if}
</label>