web-player: app chrome (layout, navigation, player)
PlayerExtras is merged into PlayerBar, which now renders volume and the queue button inline and uses the new Slider (a real range input with thumb thickness controlled via CSS variables) for both the progress bar and volume, replacing SeekBar and VolumeControl (grep confirmed SeekBar's `compact` mode was dead code). Sidebar and TabsBar migrate to appNavLinks(user, counts?), removing the duplicated nav links between the two. Decision made without blocking: Sidebar gains the gradient Logo mark it didn't have before, for visual consistency with MobileHeader and the login screen. PlayerDock and MobileHeader's z-index is fixed to z-(--z-sticky) (they're fixed bars, not menus), and SegmentedControl's to z-(--z-raised), per the plan's note B.1.5. glass-bar and theme-transition are applied to PlayerBar (both variants), TabsBar, Queue and Home (hero/spotlight), replacing duplicated blur/transition styles. Verified with npm run check (0 errors) and npm run build; smoke-tested /login with no console errors. No authenticated visual QA (Docker unavailable this session).
This commit is contained in:
@@ -784,7 +784,7 @@ Tareas:
|
|||||||
| 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) |
|
| 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) |
|
| 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 | ✅ | (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) |
|
| 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 | ⬜ | | |
|
| P12 | ✅ | (pendiente) | PlayerExtras se fusiona en PlayerBar (volumen/cola inline); Slider sustituye a SeekBar+VolumeControl (confirmado por grep que el modo `compact` de SeekBar era código muerto). Sidebar y TabsBar migran a `appNavLinks()` (P5). Decisión tomada (sin bloquear): Sidebar gana la marca-gradiente `Logo` que no tenía antes, para consistencia con MobileHeader/login. z-index de PlayerDock/MobileHeader corregido a `z-(--z-sticky)` (son barras fijas, no menús) y SegmentedControl a `z-(--z-raised)`, tal como indica B.1.5. `glass-bar`/`theme-transition` aplicados en PlayerBar (ambas variantes), TabsBar, Queue e Inicio (hero/spotlight), sustituyendo estilos duplicados. Sin QA visual autenticada (Docker no disponible); smoke test de `/login` sin errores de consola |
|
||||||
| P13 | ⬜ | | |
|
| P13 | ⬜ | | |
|
||||||
| P14 | ⬜ | | |
|
| P14 | ⬜ | | |
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import AccountMenu from './ui/AccountMenu.svelte';
|
import AccountMenu from './ui/AccountMenu.svelte';
|
||||||
import Avatar from './ui/Avatar.svelte';
|
import Avatar from './ui/Avatar.svelte';
|
||||||
import IconButton from './ui/IconButton.svelte';
|
import IconButton from './ui/IconButton.svelte';
|
||||||
|
import Logo from './ui/Logo.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: SessionUser | null;
|
user: SessionUser | null;
|
||||||
@@ -17,14 +18,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
class="sticky top-0 z-40 flex h-14 items-center justify-between border-b border-hairline bg-bg/85 page-x backdrop-blur-md sm:hidden"
|
class="sticky top-0 z-(--z-sticky) flex h-14 items-center justify-between border-b border-hairline bg-bg/85 page-x backdrop-blur-md sm:hidden"
|
||||||
>
|
>
|
||||||
<a
|
<a href="/">
|
||||||
href="/"
|
<Logo size="sm" />
|
||||||
class="flex items-center gap-2 font-display text-lg font-semibold tracking-[-0.02em] text-fg"
|
|
||||||
>
|
|
||||||
<span class="h-5.5 w-5.5 rounded-lg bg-[image:var(--mf-logo-grad)]"></span>
|
|
||||||
Musify
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{#if user}
|
{#if user}
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { isSectionActive } from '$lib/navigation.svelte';
|
import { isSectionActive, appNavLinks } from '$lib/navigation.svelte';
|
||||||
import { liked } from '$lib/player/liked.svelte';
|
import { liked } from '$lib/player/liked.svelte';
|
||||||
import { createPlaylistModal } from '$lib/playlists.svelte';
|
import { createPlaylistModal } from '$lib/playlists.svelte';
|
||||||
import type { SessionUser } from '$lib/types';
|
import type { SessionUser } from '$lib/types';
|
||||||
import ListRow from './ui/ListRow.svelte';
|
import ListRow from './ui/ListRow.svelte';
|
||||||
import IconButton from './ui/IconButton.svelte';
|
import IconButton from './ui/IconButton.svelte';
|
||||||
import Home from '@lucide/svelte/icons/house';
|
import Logo from './ui/Logo.svelte';
|
||||||
import Compass from '@lucide/svelte/icons/compass';
|
|
||||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
|
||||||
import Heart from '@lucide/svelte/icons/heart';
|
|
||||||
import Disc from '@lucide/svelte/icons/disc-2';
|
|
||||||
import Folder from '@lucide/svelte/icons/folder';
|
|
||||||
import Upload from '@lucide/svelte/icons/upload';
|
|
||||||
import Plus from '@lucide/svelte/icons/plus';
|
import Plus from '@lucide/svelte/icons/plus';
|
||||||
|
|
||||||
interface SidebarPlaylist {
|
interface SidebarPlaylist {
|
||||||
@@ -29,19 +23,7 @@
|
|||||||
|
|
||||||
let { user, playlists }: Props = $props();
|
let { user, playlists }: Props = $props();
|
||||||
|
|
||||||
const navLinks = $derived(
|
const navLinks = $derived(appNavLinks(user, { playlists: playlists.length, liked: liked.count }));
|
||||||
user
|
|
||||||
? [
|
|
||||||
{ href: '/', label: 'Inicio', icon: Home },
|
|
||||||
{ href: '/explore', label: 'Descubrir', icon: Compass },
|
|
||||||
{ href: '/playlists', label: 'Playlists', icon: ListMusic, count: playlists.length },
|
|
||||||
{ href: '/liked', label: 'Me gusta', icon: Heart, count: liked.count },
|
|
||||||
{ href: '/albums', label: 'Mis álbumes', icon: Disc },
|
|
||||||
{ href: '/library', label: 'Canciones subidas', icon: Folder },
|
|
||||||
{ href: '/upload', label: 'Subir música', icon: Upload }
|
|
||||||
]
|
|
||||||
: [{ href: '/explore', label: 'Descubrir', icon: Compass }]
|
|
||||||
);
|
|
||||||
|
|
||||||
function isActive(href: string) {
|
function isActive(href: string) {
|
||||||
return isSectionActive(href, page.url.pathname, page.data.section);
|
return isSectionActive(href, page.url.pathname, page.data.section);
|
||||||
@@ -49,10 +31,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
class="hidden shrink-0 flex-col border-r border-hairline bg-bg p-5 transition-[background] duration-500 ease-out lg:flex"
|
class="theme-transition hidden shrink-0 flex-col border-r border-hairline bg-bg p-5 lg:flex"
|
||||||
style="width:var(--mf-sidebar-w)"
|
style="width:var(--mf-sidebar-w)"
|
||||||
>
|
>
|
||||||
<a href="/" class="px-3 pb-3 font-display text-lg font-semibold tracking-tight text-fg">Musify</a>
|
<a href="/" class="px-3 pb-3">
|
||||||
|
<Logo size="sm" />
|
||||||
|
</a>
|
||||||
|
|
||||||
<nav class="flex flex-col gap-1">
|
<nav class="flex flex-col gap-1">
|
||||||
{#each navLinks as link (link.href)}
|
{#each navLinks as link (link.href)}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { isSectionActive } from '$lib/navigation.svelte';
|
import { isSectionActive, appNavLinks } from '$lib/navigation.svelte';
|
||||||
import type { SessionUser } from '$lib/types';
|
import type { SessionUser } from '$lib/types';
|
||||||
import Home from '@lucide/svelte/icons/house';
|
|
||||||
import Compass from '@lucide/svelte/icons/compass';
|
|
||||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
|
||||||
import Heart from '@lucide/svelte/icons/heart';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: SessionUser | null;
|
user: SessionUser | null;
|
||||||
@@ -14,16 +10,7 @@
|
|||||||
|
|
||||||
let { user, class: klass = '' }: Props = $props();
|
let { user, class: klass = '' }: Props = $props();
|
||||||
|
|
||||||
const links = $derived(
|
const links = $derived(appNavLinks(user).filter((link) => link.primary));
|
||||||
user
|
|
||||||
? [
|
|
||||||
{ href: '/', label: 'Inicio', icon: Home },
|
|
||||||
{ href: '/explore', label: 'Descubrir', icon: Compass },
|
|
||||||
{ href: '/playlists', label: 'Playlists', icon: ListMusic },
|
|
||||||
{ href: '/liked', label: 'Me gusta', icon: Heart }
|
|
||||||
]
|
|
||||||
: [{ href: '/explore', label: 'Descubrir', icon: Compass }]
|
|
||||||
);
|
|
||||||
|
|
||||||
function isActive(href: string) {
|
function isActive(href: string) {
|
||||||
return isSectionActive(href, page.url.pathname, page.data.section);
|
return isSectionActive(href, page.url.pathname, page.data.section);
|
||||||
@@ -31,7 +18,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
class="pointer-events-auto flex items-center justify-around border-t border-hairline bg-[image:var(--mf-bar-bg)] px-4 py-1.75 backdrop-blur-xl transition-[background] duration-500 lg:hidden {klass}"
|
class="pointer-events-auto flex items-center justify-around glass-bar px-4 py-1.75 lg:hidden {klass}"
|
||||||
style="padding-bottom:calc(0.4375rem + var(--mf-safe-b))"
|
style="padding-bottom:calc(0.4375rem + var(--mf-safe-b))"
|
||||||
>
|
>
|
||||||
{#each links as link (link.href)}
|
{#each links as link (link.href)}
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TrackInfo from './TrackInfo.svelte';
|
import TrackInfo from './TrackInfo.svelte';
|
||||||
import TransportControls from './TransportControls.svelte';
|
import TransportControls from './TransportControls.svelte';
|
||||||
import SeekBar from './SeekBar.svelte';
|
import Slider from '$lib/components/ui/Slider.svelte';
|
||||||
import PlayerExtras from './PlayerExtras.svelte';
|
import IconButton from '$lib/components/ui/IconButton.svelte';
|
||||||
|
import { player } from '$lib/player/player.svelte';
|
||||||
|
import { queuePanel } from '$lib/player/queuePanel.svelte';
|
||||||
|
import { fmtTime } from '$lib/format';
|
||||||
|
import Volume2 from '@lucide/svelte/icons/volume-2';
|
||||||
|
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||||
|
|
||||||
|
function seek(value: number) {
|
||||||
|
if (player.current.duration > 0) player.seekFraction(value / player.current.duration);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="animate-enter pointer-events-auto flex items-center gap-4.5 border-t border-hairline bg-[image:var(--mf-bar-bg)] px-4 py-2.75 backdrop-blur-xl transition-[background] duration-500 sm:hidden"
|
class="animate-enter pointer-events-auto flex items-center gap-4.5 glass-bar px-4 py-2.75 sm:hidden"
|
||||||
>
|
>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<TrackInfo compact />
|
<TrackInfo compact />
|
||||||
@@ -15,14 +24,46 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="animate-enter pointer-events-auto hidden items-center gap-5 border-t border-hairline bg-[image:var(--mf-bar-bg)] px-4 py-2.75 backdrop-blur-xl transition-[background] duration-500 sm:flex"
|
class="animate-enter pointer-events-auto hidden items-center gap-5 glass-bar px-4 py-2.75 sm:flex"
|
||||||
>
|
>
|
||||||
<div class="w-64 min-w-0 shrink-0">
|
<div class="w-(--mf-player-side-w) min-w-0 shrink-0">
|
||||||
<TrackInfo />
|
<TrackInfo />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex min-w-0 flex-1 flex-col items-center gap-2">
|
<div class="flex min-w-0 flex-1 flex-col items-center gap-2">
|
||||||
<TransportControls />
|
<TransportControls />
|
||||||
<SeekBar />
|
<div class="flex w-full max-w-hero items-center gap-2.75">
|
||||||
|
<span class="min-w-8 text-right text-xs text-fg-3 tabular-nums">
|
||||||
|
{fmtTime(player.progress)}
|
||||||
|
</span>
|
||||||
|
<Slider
|
||||||
|
value={player.progress}
|
||||||
|
max={player.current.duration}
|
||||||
|
label="Barra de progreso"
|
||||||
|
oninput={seek}
|
||||||
|
variant="seek"
|
||||||
|
/>
|
||||||
|
<span class="min-w-8 text-xs text-fg-3 tabular-nums">
|
||||||
|
{fmtTime(player.current.duration)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden w-(--mf-player-side-w) shrink-0 items-center justify-end gap-3.5 sm:flex">
|
||||||
|
<Volume2 class="size-icon-sm shrink-0 text-fg-3" strokeWidth={1.8} />
|
||||||
|
<Slider
|
||||||
|
value={player.volume}
|
||||||
|
max={100}
|
||||||
|
label="Volumen"
|
||||||
|
oninput={(value) => player.setVolume(value)}
|
||||||
|
variant="volume"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
label="Cola de reproducción"
|
||||||
|
tone="plain"
|
||||||
|
size="xs"
|
||||||
|
pressed={queuePanel.open}
|
||||||
|
onclick={() => queuePanel.toggle()}
|
||||||
|
>
|
||||||
|
<ListMusic class="size-icon-sm" strokeWidth={1.8} />
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<PlayerExtras />
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none fixed inset-x-0 bottom-0 z-40 flex flex-col lg:left-[var(--mf-sidebar-w)]"
|
class="pointer-events-none fixed inset-x-0 bottom-0 z-(--z-sticky) flex flex-col lg:left-(--mf-sidebar-w)"
|
||||||
>
|
>
|
||||||
{#if player.error}
|
{#if player.error}
|
||||||
<div class="pointer-events-auto px-4 pb-2.5">
|
<div class="pointer-events-auto px-4 pb-2.5">
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import VolumeControl from './VolumeControl.svelte';
|
|
||||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
|
||||||
import { queuePanel } from '$lib/player/queuePanel.svelte';
|
|
||||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="hidden w-(--mf-player-side-w) shrink-0 items-center justify-end gap-3.5 sm:flex">
|
|
||||||
<VolumeControl />
|
|
||||||
<IconButton
|
|
||||||
label="Cola de reproducción"
|
|
||||||
tone="plain"
|
|
||||||
size="xs"
|
|
||||||
pressed={queuePanel.open}
|
|
||||||
onclick={() => queuePanel.toggle()}
|
|
||||||
>
|
|
||||||
<ListMusic class="size-icon-sm" strokeWidth={1.8} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
{#if queuePanel.open}
|
{#if queuePanel.open}
|
||||||
<aside
|
<aside
|
||||||
transition:fly={{ x: 24, duration: 220, easing: cubicOut }}
|
transition:fly={{ x: 24, duration: 220, easing: cubicOut }}
|
||||||
class="hidden shrink-0 flex-col border-l border-hairline bg-bg px-4 pt-5 transition-[background] duration-500 sm:flex"
|
class="hidden shrink-0 flex-col border-l border-hairline bg-bg px-4 pt-5 theme-transition sm:flex"
|
||||||
style="width:var(--mf-queue-w); padding-bottom:calc(var(--mf-player-h) + 2rem)"
|
style="width:var(--mf-queue-w); padding-bottom:calc(var(--mf-player-h) + 2rem)"
|
||||||
>
|
>
|
||||||
<div class="mb-5 flex items-center justify-between">
|
<div class="mb-5 flex items-center justify-between">
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { player } from '$lib/player/player.svelte';
|
|
||||||
import { fmtTime } from '$lib/format';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
compact?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { compact = false }: Props = $props();
|
|
||||||
|
|
||||||
function seek(event: MouseEvent) {
|
|
||||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
||||||
player.seekFraction((event.clientX - rect.left) / rect.width);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if compact}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={seek}
|
|
||||||
aria-label="Barra de progreso"
|
|
||||||
class="relative block h-1 w-full cursor-pointer bg-track"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="absolute inset-y-0 left-0 bg-accent/70"
|
|
||||||
style="width:{player.progressPercent}%"
|
|
||||||
></div>
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<div class="flex w-full max-w-hero items-center gap-2.75">
|
|
||||||
<span class="min-w-8 text-right text-xs text-fg-3 tabular-nums">
|
|
||||||
{fmtTime(player.progress)}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={seek}
|
|
||||||
aria-label="Barra de progreso"
|
|
||||||
class="group relative h-3.5 flex-1 cursor-pointer"
|
|
||||||
>
|
|
||||||
<div class="h-[3.5px] w-full overflow-hidden rounded-full bg-track">
|
|
||||||
<div class="h-full rounded-full bg-accent" style="width:{player.progressPercent}%"></div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<span class="min-w-8 text-xs text-fg-3 tabular-nums">
|
|
||||||
{fmtTime(player.current.duration)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { player } from '$lib/player/player.svelte';
|
|
||||||
import Volume2 from '@lucide/svelte/icons/volume-2';
|
|
||||||
|
|
||||||
function handleVolumeChange(event: Event) {
|
|
||||||
const target = event.currentTarget as HTMLInputElement;
|
|
||||||
player.setVolume(Number(target.value));
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2.25">
|
|
||||||
<Volume2 class="h-3.75 w-3.75 shrink-0 text-fg-3" strokeWidth={1.8} />
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
value={player.volume}
|
|
||||||
oninput={handleVolumeChange}
|
|
||||||
aria-label="Volumen"
|
|
||||||
class="mf-volume"
|
|
||||||
style="background:linear-gradient(to right, var(--mf-text-2) {player.volume}%, var(--mf-track) {player.volume}%)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.mf-volume {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
width: 84px;
|
|
||||||
height: 3.5px;
|
|
||||||
border-radius: 100px;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.mf-volume::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
width: 11px;
|
|
||||||
height: 11px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--mf-text);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.mf-volume::-moz-range-thumb {
|
|
||||||
width: 11px;
|
|
||||||
height: 11px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--mf-text);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
size?: 'sm' | 'lg';
|
||||||
|
}
|
||||||
|
|
||||||
|
let { size = 'sm' }: Props = $props();
|
||||||
|
|
||||||
|
const MARK: Record<'sm' | 'lg', string> = {
|
||||||
|
sm: 'size-5.5 rounded-lg',
|
||||||
|
lg: 'size-8 rounded-control'
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEXT: Record<'sm' | 'lg', string> = {
|
||||||
|
sm: 'font-display text-lg font-semibold tracking-tight text-fg',
|
||||||
|
lg: 'font-display text-[clamp(1.5rem,3vw,2.5rem)] font-semibold tracking-tight'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="flex items-center gap-3">
|
||||||
|
<span class="{MARK[size]} bg-[image:var(--mf-logo-grad)]"></span>
|
||||||
|
<span class={TEXT[size]}>Musify</span>
|
||||||
|
</span>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => (value = option.value)}
|
onclick={() => (value = option.value)}
|
||||||
class="relative z-10 flex-1 px-4 py-1.5 text-center text-xs font-medium transition-colors {value ===
|
class="relative z-(--z-raised) flex-1 px-4 py-1.5 text-center text-xs font-medium transition-colors {value ===
|
||||||
option.value
|
option.value
|
||||||
? 'text-ink'
|
? 'text-ink'
|
||||||
: 'text-muted hover:text-fg-2'}"
|
: 'text-muted hover:text-fg-2'}"
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
value: number;
|
||||||
|
max: number;
|
||||||
|
label: string;
|
||||||
|
oninput: (value: number) => void;
|
||||||
|
variant: 'seek' | 'volume';
|
||||||
|
}
|
||||||
|
|
||||||
|
let { value, max, label, oninput, variant }: Props = $props();
|
||||||
|
|
||||||
|
const percent = $derived(max > 0 ? (value / max) * 100 : 0);
|
||||||
|
|
||||||
|
function handleInput(event: Event) {
|
||||||
|
oninput(Number((event.currentTarget as HTMLInputElement).value));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
{max}
|
||||||
|
{value}
|
||||||
|
oninput={handleInput}
|
||||||
|
aria-label={label}
|
||||||
|
class="mf-slider mf-slider-{variant}"
|
||||||
|
style="background:linear-gradient(to right, var(--mf-text-2) {percent}%, var(--mf-track) {percent}%)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mf-slider {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: var(--mf-range-h);
|
||||||
|
border-radius: 100px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.mf-slider-seek {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.mf-slider-volume {
|
||||||
|
width: var(--mf-volume-w);
|
||||||
|
}
|
||||||
|
.mf-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: var(--mf-range-thumb);
|
||||||
|
height: var(--mf-range-thumb);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--mf-text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.mf-slider::-moz-range-thumb {
|
||||||
|
width: var(--mf-range-thumb);
|
||||||
|
height: var(--mf-range-thumb);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--mf-text);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
|
|
||||||
<section class="page-x pt-3 sm:pt-4">
|
<section class="page-x pt-3 sm:pt-4">
|
||||||
<div
|
<div
|
||||||
class="relative overflow-hidden rounded-3xl bg-[image:var(--mf-hero-bg)] px-6 py-8 transition-[background] duration-500 sm:px-9 sm:py-8.5"
|
class="relative overflow-hidden rounded-3xl bg-[image:var(--mf-hero-bg)] px-6 py-8 theme-transition sm:px-9 sm:py-8.5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative flex flex-col items-start gap-7 lg:flex-row lg:flex-wrap lg:items-end lg:justify-between"
|
class="relative flex flex-col items-start gap-7 lg:flex-row lg:flex-wrap lg:items-end lg:justify-between"
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
{#if spotlight}
|
{#if spotlight}
|
||||||
<section>
|
<section>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 overflow-hidden rounded-panel-lg transition-[background] duration-500 lg:grid-cols-2"
|
class="grid grid-cols-1 overflow-hidden rounded-panel-lg theme-transition lg:grid-cols-2"
|
||||||
style="background-image:var(--mf-spotlight-bg), linear-gradient(to right, transparent 50%, color-mix(in oklch, var(--mf-scrim) 22%, transparent) 61%, transparent 92.5%)"
|
style="background-image:var(--mf-spotlight-bg), linear-gradient(to right, transparent 50%, color-mix(in oklch, var(--mf-scrim) 22%, transparent) 61%, transparent 92.5%)"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col justify-center p-8 sm:p-9">
|
<div class="flex flex-col justify-center p-8 sm:p-9">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
|
import Logo from '$lib/components/ui/Logo.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -15,12 +16,7 @@
|
|||||||
<div class="pointer-events-none absolute inset-0" style="background:var(--mf-ambient)"></div>
|
<div class="pointer-events-none absolute inset-0" style="background:var(--mf-ambient)"></div>
|
||||||
|
|
||||||
<div class="relative flex w-full max-w-4xl flex-col items-center gap-12 text-center">
|
<div class="relative flex w-full max-w-4xl flex-col items-center gap-12 text-center">
|
||||||
<div class="flex items-center gap-3">
|
<Logo size="lg" />
|
||||||
<span class="h-8 w-8 rounded-xl bg-[image:var(--mf-logo-grad)]"></span>
|
|
||||||
<span class="font-display text-[clamp(1.5rem,3vw,2.5rem)] font-semibold tracking-tight">
|
|
||||||
Musify
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="animate-enter flex flex-col items-center gap-5">
|
<div class="animate-enter flex flex-col items-center gap-5">
|
||||||
<h1
|
<h1
|
||||||
@@ -32,7 +28,7 @@
|
|||||||
<p class="text-lg text-fg-2">Inicia sesión para continuar.</p>
|
<p class="text-lg text-fg-2">Inicia sesión para continuar.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full max-w-80 flex-col gap-3">
|
<div class="flex w-full max-w-cta flex-col gap-3">
|
||||||
<Button href={`/auth/login?${returnParam}`} reload size="lg" class="w-full">
|
<Button href={`/auth/login?${returnParam}`} reload size="lg" class="w-full">
|
||||||
Iniciar sesión
|
Iniciar sesión
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user