web-player: buttons (Button, IconButton, PlayButton)
Button: subtle variants (primary/secondary, left unchanged per the user's call), glass (formerly secondaryOnGlow, with its own font-normal), accent, strong (cta-strong/ink, replacing the `!` overrides on the Home hero), danger. Uses the --color-btn* tokens instead of bg-[var(--mf-btn-...)]. Radius unified to rounded-control. IconButton: adds shape (square/round), size (xs/sm/md/lg), tone (muted/subtle/plain), pressed (aria-pressed + text-accent), href/reload (links), surface (permanent background + hover to surface-hover instead of the generic `hover` alias, which matched surface-2 and gave no contrast). New PlayButton: a single place for the play/pause SVGs, strong/glass variants, as="span" for non-interactive overlays. Replaces TransportControls' main button, the MixTile overlay, and the "Top result" circle in Explore. Migrated to IconButton: close queue, "Create playlist" (Sidebar), Like (TrackInfo, with pressed), queue (PlayerExtras), back (TopBar), login (MobileHeader), and the row actions in tables (remove/add, size xs). BackLink uses size-icon-sm and drops a duplicate hover. Decision (not blocking): remove TopBar's decorative "forward" button and PlayerExtras' decorative AlignJustify icon (visually dead code). TransportControls' buttons (shuffle/prev/next/repeat) are deliberately left unmigrated: they currently have no background or hit-box of their own, and IconButton always applies a fixed-size box — migrating them would have added a visible hover pill that couldn't be verified without Docker this session. Verified with npm run check/build.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import MenuItem from './ui/MenuItem.svelte';
|
||||
import AccountMenu from './ui/AccountMenu.svelte';
|
||||
import Avatar from './ui/Avatar.svelte';
|
||||
import IconButton from './ui/IconButton.svelte';
|
||||
|
||||
interface Props {
|
||||
user: SessionUser | null;
|
||||
@@ -50,14 +51,9 @@
|
||||
{/snippet}
|
||||
</AccountMenu>
|
||||
{:else}
|
||||
<a
|
||||
href="/auth/login"
|
||||
data-sveltekit-reload
|
||||
aria-label="Iniciar sesión"
|
||||
class="flex items-center rounded-full p-1.5 text-fg-3 transition hover:bg-hover hover:text-fg"
|
||||
>
|
||||
<LogIn class="h-6 w-6" strokeWidth={2} />
|
||||
</a>
|
||||
<IconButton href="/auth/login" reload label="Iniciar sesión" shape="round" size="md">
|
||||
<LogIn class="size-icon-lg" strokeWidth={2} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { createPlaylistModal } from '$lib/playlists.svelte';
|
||||
import type { SessionUser } from '$lib/types';
|
||||
import Artwork from './ui/Artwork.svelte';
|
||||
import IconButton from './ui/IconButton.svelte';
|
||||
import Home from '@lucide/svelte/icons/house';
|
||||
import Compass from '@lucide/svelte/icons/compass';
|
||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||
@@ -77,14 +78,14 @@
|
||||
<div class="flex items-center justify-between px-3 pb-1.5">
|
||||
<span class="text-xs font-medium tracking-widest text-muted uppercase">Tus playlists</span>
|
||||
{#if playlists.length > 0}
|
||||
<button
|
||||
type="button"
|
||||
<IconButton
|
||||
label="Crear playlist"
|
||||
tone="plain"
|
||||
size="xs"
|
||||
onclick={() => createPlaylistModal.show()}
|
||||
aria-label="Crear playlist"
|
||||
class="text-muted transition-colors hover:text-fg"
|
||||
>
|
||||
<Plus class="h-3.5 w-3.5" strokeWidth={1.25} />
|
||||
</button>
|
||||
<Plus class="size-icon-sm" strokeWidth={1.25} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||
import AccountMenu from './ui/AccountMenu.svelte';
|
||||
import Avatar from './ui/Avatar.svelte';
|
||||
import IconButton from './ui/IconButton.svelte';
|
||||
import Button from './ui/Button.svelte';
|
||||
|
||||
interface Props {
|
||||
user: SessionUser | null;
|
||||
@@ -56,24 +57,18 @@
|
||||
</script>
|
||||
|
||||
<header class="hidden items-center gap-3.5 px-6.5 pt-4 pb-3 sm:flex">
|
||||
<div class="flex shrink-0 gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => history.back()}
|
||||
aria-label="Atrás"
|
||||
class="grid h-7.5 w-7.5 place-items-center rounded-full bg-surface-2 text-fg-2 transition-colors hover:bg-surface-hover hover:text-fg"
|
||||
>
|
||||
<ChevronLeft class="h-3.75 w-3.75" strokeWidth={1.9} />
|
||||
</button>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="grid h-7.5 w-7.5 place-items-center rounded-full bg-surface-2 text-muted"
|
||||
>
|
||||
<ChevronRight class="h-3.75 w-3.75" strokeWidth={1.9} />
|
||||
</span>
|
||||
</div>
|
||||
<IconButton
|
||||
label="Atrás"
|
||||
shape="round"
|
||||
tone="subtle"
|
||||
surface
|
||||
size="xs"
|
||||
onclick={() => history.back()}
|
||||
>
|
||||
<ChevronLeft class="size-icon-sm" strokeWidth={1.9} />
|
||||
</IconButton>
|
||||
|
||||
<form onsubmit={onSubmit} class="max-w-120 min-w-0 flex-1">
|
||||
<form onsubmit={onSubmit} class="max-w-search min-w-0 flex-1">
|
||||
<label
|
||||
class="flex h-9.5 w-full items-center gap-2.5 rounded-control bg-surface-2 px-3.5 transition focus-within:bg-surface-hover"
|
||||
>
|
||||
@@ -126,13 +121,9 @@
|
||||
{/snippet}
|
||||
</AccountMenu>
|
||||
{:else}
|
||||
<a
|
||||
href="/auth/login"
|
||||
data-sveltekit-reload
|
||||
class="shrink-0 rounded-full bg-accent-soft px-4 py-2 text-sm font-semibold text-ink transition hover:brightness-110"
|
||||
<Button href="/auth/login" reload variant="accent" size="sm" class="shrink-0"
|
||||
>Iniciar sesión</Button
|
||||
>
|
||||
Iniciar sesión
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<script lang="ts">
|
||||
import VolumeControl from './VolumeControl.svelte';
|
||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
||||
import { queuePanel } from '$lib/player/queuePanel.svelte';
|
||||
import AlignJustify from '@lucide/svelte/icons/align-justify';
|
||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||
</script>
|
||||
|
||||
<div class="hidden w-64 shrink-0 items-center justify-end gap-3.5 sm:flex">
|
||||
<span aria-hidden="true" class="text-fg-3">
|
||||
<AlignJustify class="h-3.75 w-3.75" strokeWidth={1.8} />
|
||||
</span>
|
||||
<div class="hidden w-(--mf-player-side-w) shrink-0 items-center justify-end gap-3.5 sm:flex">
|
||||
<VolumeControl />
|
||||
<button
|
||||
type="button"
|
||||
<IconButton
|
||||
label="Cola de reproducción"
|
||||
tone="plain"
|
||||
size="xs"
|
||||
pressed={queuePanel.open}
|
||||
onclick={() => queuePanel.toggle()}
|
||||
aria-pressed={queuePanel.open}
|
||||
aria-label="Cola de reproducción"
|
||||
class="transition-colors hover:text-fg {queuePanel.open ? 'text-fg' : 'text-fg-2'}"
|
||||
>
|
||||
<ListMusic class="h-4 w-4" strokeWidth={1.8} />
|
||||
</button>
|
||||
<ListMusic class="size-icon-sm" strokeWidth={1.8} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { queuePanel } from '$lib/player/queuePanel.svelte';
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import ArtistLink from '$lib/components/ui/ArtistLink.svelte';
|
||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
||||
import { fmtTime } from '$lib/format';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import { fly } from 'svelte/transition';
|
||||
@@ -22,14 +23,9 @@
|
||||
>
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<h2 class="font-display text-base font-medium tracking-tight text-fg">En cola</h2>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => queuePanel.close()}
|
||||
aria-label="Cerrar cola"
|
||||
class="text-fg-3 transition-colors hover:text-fg"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</button>
|
||||
<IconButton label="Cerrar cola" tone="plain" size="xs" onclick={() => queuePanel.close()}>
|
||||
<X class="size-icon-md" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
{#if player.current.id}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||
import ExplicitBadge from '$lib/components/ui/ExplicitBadge.svelte';
|
||||
import ArtistLink from '$lib/components/ui/ArtistLink.svelte';
|
||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
||||
import Heart from '@lucide/svelte/icons/heart';
|
||||
|
||||
interface Props {
|
||||
@@ -52,14 +53,14 @@
|
||||
/>
|
||||
</div>
|
||||
{#if !compact}
|
||||
<button
|
||||
type="button"
|
||||
<IconButton
|
||||
label={isLiked ? 'Quitar de Me gusta' : 'Añadir a Me gusta'}
|
||||
tone="plain"
|
||||
size="xs"
|
||||
pressed={isLiked}
|
||||
onclick={toggleLike}
|
||||
aria-label={isLiked ? 'Quitar de Me gusta' : 'Añadir a Me gusta'}
|
||||
aria-pressed={isLiked}
|
||||
class="shrink-0 transition hover:opacity-80 {isLiked ? 'text-accent' : 'text-fg-3'}"
|
||||
>
|
||||
<Heart class="h-4 w-4" fill={isLiked ? 'currentColor' : 'none'} strokeWidth={1.7} />
|
||||
</button>
|
||||
<Heart class="size-icon-md" fill={isLiked ? 'currentColor' : 'none'} strokeWidth={1.7} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import PlayButton from '$lib/components/ui/PlayButton.svelte';
|
||||
import Shuffle from '@lucide/svelte/icons/shuffle';
|
||||
import Repeat from '@lucide/svelte/icons/repeat';
|
||||
|
||||
@@ -36,23 +37,11 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<PlayButton
|
||||
playing={player.playing}
|
||||
onclick={() => player.toggle()}
|
||||
aria-label={player.playing ? 'Pausar' : 'Reproducir'}
|
||||
class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-cta-strong text-ink transition duration-150 hover:brightness-95"
|
||||
>
|
||||
{#if player.playing}
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="currentColor">
|
||||
<rect x="3" y="1" width="3" height="13" rx="1" />
|
||||
<rect x="9" y="1" width="3" height="13" rx="1" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="currentColor">
|
||||
<path d="M3 1.5v12l10-6z" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
label={player.playing ? 'Pausar' : 'Reproducir'}
|
||||
/>
|
||||
|
||||
<button type="button" onclick={() => player.next()} aria-label="Siguiente" class={sideClass}>
|
||||
<svg width="16" height="14" viewBox="0 0 16 14" fill="currentColor">
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
<a
|
||||
href={target.url}
|
||||
{onclick}
|
||||
class="group inline-flex items-center text-fg-3 transition-colors hover:text-accent-soft"
|
||||
class="inline-flex items-center text-fg-3 transition-colors hover:text-accent-soft"
|
||||
>
|
||||
<span class="grid h-8 w-8 place-items-center transition-colors group-hover:text-accent-soft">
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
<span class="grid size-8 place-items-center">
|
||||
<ArrowLeft class="size-icon-sm" />
|
||||
</span>
|
||||
{target.label}
|
||||
</a>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
||||
|
||||
interface Props {
|
||||
variant?: 'primary' | 'secondary' | 'secondaryOnGlow' | 'accent' | 'danger';
|
||||
variant?: 'primary' | 'secondary' | 'glass' | 'accent' | 'strong' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
type?: 'button' | 'submit';
|
||||
href?: string;
|
||||
@@ -34,21 +34,20 @@
|
||||
lg: 'px-6 py-3.5 text-sm'
|
||||
};
|
||||
|
||||
const subtleButton =
|
||||
'bg-[var(--mf-btn-bg)] text-fg-2 hover:bg-[var(--mf-btn-bg-hover)] hover:text-fg';
|
||||
const subtleButtonOnGlow =
|
||||
'bg-[var(--mf-btn-secondary-bg)] text-fg-2 hover:bg-[var(--mf-btn-secondary-bg-hover)] hover:text-fg';
|
||||
const subtle = 'bg-btn text-fg-2 hover:bg-btn-hover hover:text-fg';
|
||||
const glass = 'font-normal bg-btn-glass text-fg-2 hover:bg-btn-glass-hover hover:text-fg';
|
||||
|
||||
const variants = {
|
||||
primary: subtleButton,
|
||||
secondary: subtleButton,
|
||||
secondaryOnGlow: subtleButtonOnGlow,
|
||||
primary: subtle,
|
||||
secondary: subtle,
|
||||
glass,
|
||||
accent: 'bg-accent-btn text-accent-soft hover:bg-accent-btn-hover',
|
||||
strong: 'bg-cta-strong text-ink hover:brightness-95',
|
||||
danger: 'bg-danger text-on-art hover:brightness-110'
|
||||
};
|
||||
|
||||
const classes = $derived(
|
||||
`inline-flex items-center justify-center gap-2 rounded-xl transition focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none disabled:opacity-40 ${sizes[size]} ${variants[variant]} ${klass}`
|
||||
`inline-flex items-center justify-center gap-2 rounded-control transition focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none disabled:opacity-40 ${sizes[size]} ${variants[variant]} ${klass}`
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
interface Props {
|
||||
label: string;
|
||||
type?: 'button' | 'submit';
|
||||
size?: 'sm' | 'md';
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
shape?: 'square' | 'round';
|
||||
tone?: 'muted' | 'subtle' | 'plain';
|
||||
pressed?: boolean;
|
||||
surface?: boolean;
|
||||
revealOnHover?: boolean;
|
||||
href?: string;
|
||||
reload?: boolean;
|
||||
onclick?: (event: MouseEvent) => void;
|
||||
class?: string;
|
||||
children: Snippet;
|
||||
@@ -14,26 +20,55 @@
|
||||
let {
|
||||
label,
|
||||
type = 'button',
|
||||
size = 'md',
|
||||
size = 'sm',
|
||||
shape = 'square',
|
||||
tone = 'muted',
|
||||
pressed,
|
||||
surface = false,
|
||||
revealOnHover = false,
|
||||
href,
|
||||
reload = false,
|
||||
onclick,
|
||||
class: klass = '',
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
const sizes = { sm: 'h-7 w-7', md: 'h-8 w-8' };
|
||||
const SIZE: Record<'xs' | 'sm' | 'md' | 'lg', string> = {
|
||||
xs: 'size-7',
|
||||
sm: 'size-8',
|
||||
md: 'size-9',
|
||||
lg: 'size-10'
|
||||
};
|
||||
|
||||
const TEXT: Record<'muted' | 'subtle' | 'plain', string> = {
|
||||
muted: 'text-muted hover:text-fg',
|
||||
subtle: 'text-fg-2 hover:text-fg',
|
||||
plain: 'text-fg-2 hover:text-fg'
|
||||
};
|
||||
|
||||
const background = $derived(
|
||||
surface ? 'bg-surface-2 hover:bg-surface-hover' : tone === 'plain' ? '' : 'hover:bg-hover'
|
||||
);
|
||||
|
||||
const classes = $derived(
|
||||
`grid place-items-center transition focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none ${SIZE[size]} ${shape === 'round' ? 'rounded-full' : 'rounded-control'} ${pressed ? 'text-accent' : TEXT[tone]} ${background} ${
|
||||
revealOnHover ? 'sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100' : ''
|
||||
} ${klass}`
|
||||
);
|
||||
</script>
|
||||
|
||||
<button
|
||||
{type}
|
||||
{onclick}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
class="grid place-items-center rounded-control text-muted transition hover:bg-hover hover:text-fg focus-visible:bg-hover focus-visible:text-fg focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none {sizes[
|
||||
size
|
||||
]} {revealOnHover
|
||||
? 'sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100'
|
||||
: ''} {klass}"
|
||||
>
|
||||
{@render children()}
|
||||
</button>
|
||||
{#if href}
|
||||
<a
|
||||
{href}
|
||||
data-sveltekit-reload={reload ? '' : undefined}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
class={classes}
|
||||
>
|
||||
{@render children()}
|
||||
</a>
|
||||
{:else}
|
||||
<button {type} {onclick} aria-label={label} title={label} aria-pressed={pressed} class={classes}>
|
||||
{@render children()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Play from '@lucide/svelte/icons/play';
|
||||
import Artwork from './Artwork.svelte';
|
||||
import PlayButton from './PlayButton.svelte';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { queueItemForTarget } from '$lib/tracks';
|
||||
import { mixItemTarget, type Mix } from '$lib/mixes';
|
||||
@@ -36,14 +36,13 @@
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-linear-to-b from-transparent from-45% to-scrim/40"
|
||||
></div>
|
||||
<button
|
||||
type="button"
|
||||
<PlayButton
|
||||
variant="glass"
|
||||
size="sm"
|
||||
onclick={play}
|
||||
aria-label="Reproducir {mix.title}"
|
||||
class="absolute right-2.5 bottom-2.5 grid h-9.5 w-9.5 place-items-center rounded-full border border-on-art/12 bg-ink/76 text-fg opacity-0 backdrop-blur transition-opacity duration-150 group-hover/tile:opacity-100"
|
||||
>
|
||||
<Play class="h-3.5 w-3.5" fill="currentColor" />
|
||||
</button>
|
||||
label="Reproducir {mix.title}"
|
||||
class="absolute right-2.5 bottom-2.5 opacity-0 transition-opacity duration-150 group-hover/tile:opacity-100"
|
||||
/>
|
||||
</Artwork>
|
||||
<div class="mt-3.25 truncate text-sm font-semibold text-fg">
|
||||
{mix.title}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
playing?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'strong' | 'glass';
|
||||
label: string;
|
||||
onclick?: (event: MouseEvent) => void;
|
||||
as?: 'button' | 'span';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
playing = false,
|
||||
size = 'md',
|
||||
variant = 'strong',
|
||||
label,
|
||||
onclick,
|
||||
as = 'button',
|
||||
class: klass = ''
|
||||
}: Props = $props();
|
||||
|
||||
const SIZE: Record<'sm' | 'md' | 'lg', string> = {
|
||||
sm: 'size-9.5',
|
||||
md: 'size-10',
|
||||
lg: 'size-11.5'
|
||||
};
|
||||
|
||||
const VARIANT: Record<'strong' | 'glass', string> = {
|
||||
strong: 'bg-cta-strong text-ink hover:brightness-95',
|
||||
glass: 'border border-on-art/12 bg-ink/76 text-fg backdrop-blur'
|
||||
};
|
||||
|
||||
const classes = $derived(
|
||||
`grid shrink-0 place-items-center rounded-full transition duration-150 ${SIZE[size]} ${VARIANT[variant]} ${klass}`
|
||||
);
|
||||
</script>
|
||||
|
||||
{#snippet icon()}
|
||||
{#if playing}
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="currentColor">
|
||||
<rect x="3" y="1" width="3" height="13" rx="1" />
|
||||
<rect x="9" y="1" width="3" height="13" rx="1" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="currentColor">
|
||||
<path d="M3 1.5v12l10-6z" />
|
||||
</svg>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if as === 'span'}
|
||||
<span aria-hidden="true" class={classes}>
|
||||
{@render icon()}
|
||||
</span>
|
||||
{:else}
|
||||
<button type="button" {onclick} aria-label={label} class={classes}>
|
||||
{@render icon()}
|
||||
</button>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user