web-player: forms (CoverForm, SegmentedControl, Chip)
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).
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import Disc3 from '@lucide/svelte/icons/disc-3';
|
||||
import ImageDropzone from './ImageDropzone.svelte';
|
||||
import Button from './Button.svelte';
|
||||
import CoverForm from './CoverForm.svelte';
|
||||
import Field from './Field.svelte';
|
||||
import Input from './Input.svelte';
|
||||
import Textarea from './Textarea.svelte';
|
||||
import Alert from './Alert.svelte';
|
||||
import { ALBUM_EARLIEST_YEAR } from '$lib/config';
|
||||
|
||||
interface Props {
|
||||
@@ -43,81 +40,55 @@
|
||||
let title = $state(untrack(() => initialTitle));
|
||||
let description = $state(untrack(() => initialDescription));
|
||||
let releaseYear = $state(untrack(() => (initialReleaseYear ? String(initialReleaseYear) : '')));
|
||||
let submitting = $state(false);
|
||||
|
||||
const latestYear = new Date().getFullYear() + 1;
|
||||
</script>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
<CoverForm
|
||||
{action}
|
||||
enctype="multipart/form-data"
|
||||
use:enhance={() => {
|
||||
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}
|
||||
>
|
||||
<div class="flex flex-col items-stretch gap-5 sm:flex-row sm:gap-8">
|
||||
<Field>
|
||||
<ImageDropzone
|
||||
name="cover"
|
||||
fallbackUrl={coverFallbackUrl}
|
||||
required={requireCover}
|
||||
icon={Disc3}
|
||||
gradient
|
||||
class="h-36 w-36 rounded-2xl"
|
||||
{#snippet fields()}
|
||||
<Field label="Título" for="album-title">
|
||||
<Input
|
||||
id="album-title"
|
||||
name="title"
|
||||
maxlength={200}
|
||||
required
|
||||
bind:value={title}
|
||||
placeholder={titlePlaceholder}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div class="flex flex-1 flex-col gap-4">
|
||||
<Field label="Título" for="album-title">
|
||||
<Input
|
||||
id="album-title"
|
||||
name="title"
|
||||
maxlength={200}
|
||||
required
|
||||
bind:value={title}
|
||||
placeholder={titlePlaceholder}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="Año de publicación" for="album-year">
|
||||
<Input
|
||||
id="album-year"
|
||||
name="releaseYear"
|
||||
type="number"
|
||||
min={ALBUM_EARLIEST_YEAR}
|
||||
max={latestYear}
|
||||
bind:value={releaseYear}
|
||||
placeholder={String(new Date().getFullYear())}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Field label="Descripción" for="album-description">
|
||||
<Textarea
|
||||
id="album-description"
|
||||
name="description"
|
||||
maxlength={256}
|
||||
bind:value={description}
|
||||
placeholder="¿De qué va este álbum?"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{#if formMessage}
|
||||
<Alert tone="danger">{formMessage}</Alert>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<Button type="button" variant="secondary" onclick={onCancel}>Cancelar</Button>
|
||||
<Button type="submit" disabled={submitting || title.trim() === ''}>
|
||||
{submitting ? submittingLabel : submitLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<Field label="Año de publicación" for="album-year">
|
||||
<Input
|
||||
id="album-year"
|
||||
name="releaseYear"
|
||||
type="number"
|
||||
min={ALBUM_EARLIEST_YEAR}
|
||||
max={latestYear}
|
||||
bind:value={releaseYear}
|
||||
placeholder={String(new Date().getFullYear())}
|
||||
/>
|
||||
</Field>
|
||||
{/snippet}
|
||||
{#snippet extra()}
|
||||
<Field label="Descripción" for="album-description">
|
||||
<Textarea
|
||||
id="album-description"
|
||||
name="description"
|
||||
maxlength={256}
|
||||
bind:value={description}
|
||||
placeholder="¿De qué va este álbum?"
|
||||
/>
|
||||
</Field>
|
||||
{/snippet}
|
||||
</CoverForm>
|
||||
|
||||
Reference in New Issue
Block a user