diff --git a/web-player/CLAUDE.md b/web-player/CLAUDE.md index a61cf1b..7651a13 100644 --- a/web-player/CLAUDE.md +++ b/web-player/CLAUDE.md @@ -79,7 +79,7 @@ The shell in `src/routes/(app)/+layout.svelte` is a fixed `h-dvh` frame: Sidebar ## Motion -Every animation follows one scale of three durations and one easing curve, all defined in `theme.css`. Fast (`--mf-motion-fast`, 150ms) is for hover, focus and press feedback: color, opacity and small scale changes. Plain `transition` or `transition-colors` already uses it because it is the Tailwind default, so never write `duration-150`. Base (`--mf-motion-base`, 200ms) is for things that appear on top of the page or move inside a control: modals, menus, the queue panel, `animate-pop`, `animate-fade` and the segmented control thumb. Slow (`--mf-motion-slow`, 300ms) is for content entering the page and larger movement: `animate-enter`, cover zoom on hover. +Every animation follows one scale of three durations and one easing curve, all defined in `theme.css`. Fast (`--mf-motion-fast`, 150ms) is for hover, focus and press feedback: color, opacity and small scale changes. Plain `transition` or `transition-colors` already uses it because it is the Tailwind default, so never write `duration-150`. Base (`--mf-motion-base`, 200ms) is for things that appear on top of the page or move inside a control: modals, menus, the queue panel, `animate-pop`, `animate-fade` and the segmented control thumb. Slow (`--mf-motion-slow`, 300ms) is for content entering the page and larger movement: `animate-enter`. Media cards have no cover zoom: hovering one only fades in its background (`hover:bg-hover`) and reveals the play button. Entrances and movement always use `--mf-ease` (`ease-snappy` in Tailwind, `expoOut` in Svelte transitions), which feels immediate because most of the motion happens at the start. Color transitions keep the Tailwind default curve. Overlays must feel instant: the modal backdrop uses `transition:fade={{ duration: 200 }}` and the panel uses a local `pop` transition (opacity + scale + translateY, `expoOut`, 200ms) mirroring `animate-pop`'s keyframes — both directives so the modal fades and pops on close too, not just on open, and the whole thing settles in 200ms. Staggered lists use `--i` with 40ms steps capped at ten items. diff --git a/web-player/scripts/check-tokens.mjs b/web-player/scripts/check-tokens.mjs index 4f2e14e..928ab06 100644 --- a/web-player/scripts/check-tokens.mjs +++ b/web-player/scripts/check-tokens.mjs @@ -15,6 +15,7 @@ const ARBITRARY_WHITELIST = [ /^my-\[\d+d?vh\]$/, // +error.svelte: centrado vertical de página de error /^py-\[\d+d?vh\]$/, // Explorar: centrado vertical de "sin resultados" /^transition-\[filter\]$/, // Explorar: genre-tile hover + /^transition-\[transform,width\]$/, // Tabs: indicador deslizante /^bg-\[image:var\(--mf-[\w-]+\)\]$/ ]; diff --git a/web-player/src/lib/components/music/GenrePicker.svelte b/web-player/src/lib/components/music/GenrePicker.svelte new file mode 100644 index 0000000..350b2fb --- /dev/null +++ b/web-player/src/lib/components/music/GenrePicker.svelte @@ -0,0 +1,58 @@ + + + +
+ {#each options as option (option.genre)} + toggle(option.genre)} + > + {option.label} + + {/each} +
+ {#if name} + {#each selected as genre (genre)} + + {/each} + {/if} + {#snippet hint()} +

{help}

+ {selected.length} elegidos + {/snippet} +
diff --git a/web-player/src/lib/components/music/ManageTrackList.svelte b/web-player/src/lib/components/music/ManageTrackList.svelte deleted file mode 100644 index b5b72c4..0000000 --- a/web-player/src/lib/components/music/ManageTrackList.svelte +++ /dev/null @@ -1,143 +0,0 @@ - - - - -
-
- allSelected, toggleAll} /> - Título - {#each COLUMNS as column (column.id)} - - {/each} - - -
-
- {#each tracks as track, i (track.id)} - {@const active = player.currentId === track.id} -
- selected.has(track.id), (checked) => toggle(track.id, checked)} - /> -
onPlay(i)} - class="flex min-w-0 flex-1 text-left" - > - - {#snippet overlay()} - {#if active} - - {/if} - {/snippet} - -
- {#each COLUMNS as column (column.id)} - - {/each} - - {fmtTime(track.duration)} - - onDelete(track)}> - - -
- {/each} -
-
diff --git a/web-player/src/lib/components/music/TrackList.svelte b/web-player/src/lib/components/music/TrackList.svelte index 38e7ac7..d892982 100644 --- a/web-player/src/lib/components/music/TrackList.svelte +++ b/web-player/src/lib/components/music/TrackList.svelte @@ -1,20 +1,34 @@ -
-
- {#if index} - - {/if} - Título - {#each columns as column (column)} - - {/each} - - {#if rowAction} - - {/if} -
-
- {#each tracks as track, i (track.id)} - {@const active = player.currentId === track.id} - -
oncontextmenu(event, track, i) : undefined} + track.id} + gap="lg" + rowClass={(track) => (player.currentId === track.id ? 'surface-active' : 'hover:bg-hover')} + oncontextmenu={oncontextmenu ? (event, track, i) => oncontextmenu(event, track, i) : undefined} +> + {#snippet head()} + + {/snippet} + {#snippet cell(track, column, i)} + {@const active = player.currentId === track.id} + {#if column.id === 'index'} + + {#if active} + + {:else} + {i + 1} +
- {/each} -
-
+ + + {/if} + {/if} + {/snippet} + diff --git a/web-player/src/lib/components/music/TrackStatusBadge.svelte b/web-player/src/lib/components/music/TrackStatusBadge.svelte new file mode 100644 index 0000000..d22ba28 --- /dev/null +++ b/web-player/src/lib/components/music/TrackStatusBadge.svelte @@ -0,0 +1,21 @@ + + + diff --git a/web-player/src/lib/components/music/TrackTitleCell.svelte b/web-player/src/lib/components/music/TrackTitleCell.svelte new file mode 100644 index 0000000..1fd0492 --- /dev/null +++ b/web-player/src/lib/components/music/TrackTitleCell.svelte @@ -0,0 +1,33 @@ + + +
+ + {#snippet overlay()} + {#if eqOverlay && active} + + {/if} + {/snippet} + +
diff --git a/web-player/src/lib/components/music/library/AlbumTrackPicker.svelte b/web-player/src/lib/components/music/library/AlbumTrackPicker.svelte new file mode 100644 index 0000000..9b84fea --- /dev/null +++ b/web-player/src/lib/components/music/library/AlbumTrackPicker.svelte @@ -0,0 +1,63 @@ + + + + + +
+

Canciones

+

+ {inAlbum.length > 0 ? plural(inAlbum.length, 'canción', 'canciones') : 'Sin canciones'} +

+ + {#if available.length > 0} +

Añadir desde tu biblioteca

+ + {/if} + +
+ {#if inAlbum.length > 0} +

En este álbum

+ + {:else} + + {/if} +
+
diff --git a/web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte b/web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte index b3aaaa9..05a8796 100644 --- a/web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte +++ b/web-player/src/lib/components/music/library/LibraryAlbumEditor.svelte @@ -1,184 +1,57 @@ - - -
-
{ - submitting = true; - return async ({ update, result }) => { - await update({ reset: false }); - submitting = false; - if (result.type === 'success') onClose(); - }; - }} - class="mt-6 grid gap-8 lg:grid-cols-[auto_1fr]" - > + + {#snippet aside()} - - - -
- -

Las canciones del álbum usan esta portada.

- - - - - - - - - - - - - - - - -
- -
-
-
-

Canciones

-

- {inAlbum.length > 0 ? plural(inAlbum.length, 'canción', 'canciones') : 'Sin canciones'} -

-
- -
- - {#if available.length > 0} -

Añadir desde tu biblioteca

- - {/if} - -
- {#if inAlbum.length > 0} -

En este álbum

- - {:else} - - {/if} -
- - {#if formMessage} - {formMessage} - {/if} - -
- - {#if !isNew} - - {/if} -
-
- -
+ + {/snippet} + {#snippet footer()} + {#if album} + + {/if} + {/snippet} + {#if album} + const FADE = { duration: 200 }; + + + + + + track.id} + stagger + rowClass={(track) => (player.currentId === track.id ? 'surface-active' : 'hover:bg-hover')} +> + {#snippet head()} + allSelected, toggleAll} /> + {/snippet} + {#snippet cell(track, column, i)} + {#if column.id === 'select'} + selected.has(track.id), (checked) => toggle(track.id, checked)} + /> + {:else if column.id === 'title'} + onPlay(i)} /> + {:else if column.id === 'album'} + {track.albumTitle ?? '-'} + {:else if column.id === 'genres'} + {genreLabels(track.genres)} + {:else if column.id === 'status'} + + {:else if column.id === 'duration'} + {fmtTime(track.duration)} + {:else} + onDelete(track)}> + + + {/if} + {/snippet} + diff --git a/web-player/src/lib/components/music/library/TrackPickList.svelte b/web-player/src/lib/components/music/library/TrackPickList.svelte index 71ec02d..ff4a6c5 100644 --- a/web-player/src/lib/components/music/library/TrackPickList.svelte +++ b/web-player/src/lib/components/music/library/TrackPickList.svelte @@ -1,15 +1,22 @@ -
-
- Título - - - - - Estado - -
-
- {#each items as item (item.id)} -
-
- - {item.format} - -
- onTitle(item.id, event.currentTarget.value)} - aria-label="Título de {item.file.name}" - class="w-full truncate rounded-tag bg-transparent text-sm text-fg transition-colors hover:bg-surface focus:bg-surface-2 focus:outline-none" - /> -

{item.file.name}

-
+ item.id} + rowClass={() => 'hover:bg-hover'} +> + {#snippet cell(item, column)} + {#if column.id === 'title'} +
+ + {item.format} + +
+ onTitle(item.id, event.currentTarget.value)} + aria-label="Título de {item.file.name}" + class="w-full truncate rounded-tag bg-transparent text-sm text-fg transition-colors hover:bg-surface focus:bg-surface-2 focus:outline-none" + /> +

{item.file.name}

+

{details(item)}

- - - - - - {#if item.error} - - {:else} - - {/if} - - onRemove(item.id)}> - -
- {/each} -
-
+ {:else if column.id === 'genres'} + {genresLabel} + {:else if column.id === 'access'} + {visibilityLabel} + {:else if column.id === 'size'} + {formatSize(item.file.size) || '-'} + {:else if column.id === 'duration'} + {item.duration === null ? '-' : fmtTime(item.duration)} + {:else if column.id === 'status'} + {#if item.error} + + {:else} + + {/if} + {:else} + onRemove(item.id)}> + + + {/if} + {/snippet} + diff --git a/web-player/src/lib/components/music/library/UploadSettings.svelte b/web-player/src/lib/components/music/library/UploadSettings.svelte index dfefef3..48481b8 100644 --- a/web-player/src/lib/components/music/library/UploadSettings.svelte +++ b/web-player/src/lib/components/music/library/UploadSettings.svelte @@ -1,11 +1,19 @@ + +