Simplify design

This commit is contained in:
2026-09-23 01:33:25 +02:00
parent d7199b4d1d
commit 2660c124d5
49 changed files with 356 additions and 195 deletions
@@ -80,7 +80,7 @@
<div class="flex items-center justify-between gap-4">
{#if helperText}
<p class="text-sm text-muted">{helperText}</p>
<p class="text-sm text-fg-2">{helperText}</p>
{:else}
<span></span>
{/if}
@@ -68,6 +68,6 @@
class="h-full w-full object-cover"
/>
{:else}
<Icon class="size-icon-xl {gradient ? 'text-on-art/70' : 'text-muted'}" strokeWidth={1.3} />
<Icon class="size-icon-xl {gradient ? 'text-on-art/70' : 'text-fg-2'}" strokeWidth={1.3} />
{/if}
</label>
@@ -88,7 +88,7 @@
<div class="flex items-center justify-between gap-4 rounded-art bg-surface px-4 py-3.5">
<div class="min-w-0">
<div class="text-sm font-medium text-fg-2">Visibilidad</div>
<div class="mt-0.5 text-xs text-muted">
<div class="mt-0.5 text-xs text-fg-2">
{visibility === 'private'
? 'Solo tú la ves en tu biblioteca'
: 'Cualquiera con el enlace puede verla'}
@@ -30,21 +30,21 @@
{title}
</h2>
{#if subtitle}
<p class="mt-1 text-xs text-fg-3">{subtitle}</p>
<p class="mt-1 text-xs text-fg-2">{subtitle}</p>
{/if}
{/snippet}
{#snippet autoActions()}
{#if count !== undefined}
<div class="h-px flex-1 self-center bg-line"></div>
<span class="shrink-0 text-xs text-muted tabular-nums">
<span class="shrink-0 text-xs text-fg-2 tabular-nums">
{plural(count, 'resultado', 'resultados')}
</span>
{/if}
{#if href}
<a
{href}
class="shrink-0 text-xs font-medium tracking-wider text-fg-3 uppercase transition-colors hover:text-fg"
class="shrink-0 text-xs font-medium tracking-wider text-fg-2 uppercase transition-colors hover:text-fg"
>
{linkLabel}
</a>
@@ -118,7 +118,7 @@
/>
{:else if fallback === 'music'}
<div class="grid h-full w-full place-items-center bg-surface">
<svg viewBox="0 0 24 24" fill="currentColor" class="h-2/5 w-2/5 text-fg-3">
<svg viewBox="0 0 24 24" fill="currentColor" class="h-2/5 w-2/5 text-fg-2">
<path
d="M19.952 1.651a.75.75 0 0 1 .298.599V16.303a3 3 0 0 1-2.176 2.884l-1.32.377a2.553 2.553 0 1 1-1.401-4.909l2.311-.66a1.5 1.5 0 0 0 1.088-1.442V6.994l-9 2.572v9.737a3 3 0 0 1-2.176 2.884l-1.32.377a2.553 2.553 0 1 1-1.402-4.909l2.312-.66a1.5 1.5 0 0 0 1.088-1.442V5.25a.75.75 0 0 1 .544-.721l10.5-3a.75.75 0 0 1 .754.122Z"
/>
@@ -7,7 +7,7 @@
</script>
<span
class="grid size-4 shrink-0 place-items-center rounded-tag bg-fg-3 text-xs leading-4 font-bold text-bg {klass}"
class="grid size-4 shrink-0 place-items-center rounded-tag bg-fg-2 text-xs leading-4 font-bold text-bg {klass}"
title="Contenido explícito"
aria-label="Contenido explícito">E</span
>
@@ -50,7 +50,7 @@
const classes = $derived(
`flex items-center gap-3 text-left transition-colors ${
variant === 'card' ? 'rounded-art p-2 pr-3.5' : 'rounded-control p-2'
} ${active ? 'bg-accent-tint' : 'hover:bg-hover'} ${klass}`
} ${active ? 'surface-active' : 'hover:bg-hover'} ${klass}`
);
</script>
@@ -38,7 +38,7 @@
{trackIds}
size="fill"
alt={title}
class="rounded-art transition duration-300 group-hover:scale-105"
class="rounded-art transition duration-300 ease-snappy group-hover:scale-105"
/>
{/if}
{#if onPlay}
@@ -50,12 +50,12 @@
size="sm"
onclick={onPlay}
label="Reproducir {title}"
class="absolute right-2.5 bottom-2.5 opacity-0 transition-opacity duration-150 group-hover:opacity-100"
class="absolute right-2.5 bottom-2.5 opacity-0 transition-opacity group-hover:opacity-100"
/>
{/if}
</div>
<div class="mt-2.5 truncate text-sm text-fg">{title}</div>
{#if subtitle}
<div class="mt-0.5 truncate text-xs text-fg-3">{subtitle}</div>
<div class="mt-0.5 truncate text-xs text-fg-2">{subtitle}</div>
{/if}
</a>
@@ -63,6 +63,6 @@
>
{@render badge?.()}
</div>
<ArtistLink name={artist} {ownerUserId} class="mt-0.5 text-xs text-fg-3" />
<ArtistLink name={artist} {ownerUserId} class="mt-0.5 text-xs text-fg-2" />
</div>
</div>
@@ -26,12 +26,12 @@
};
const VARIANT: Record<'strong' | 'glass', string> = {
strong: 'bg-cta-strong text-ink hover:brightness-95',
strong: 'bg-cta-strong text-ink hover:bg-cta-strong-hover',
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}`
`grid shrink-0 place-items-center rounded-full transition ${SIZE[size]} ${VARIANT[variant]} ${klass}`
);
</script>
@@ -91,7 +91,7 @@
<div class="overflow-x-auto {klass}">
<div
class="grid track-grid items-center gap-3 border-b border-line pr-4 pb-3 pl-2 text-sm text-fg-3 sm:gap-8 sm:pr-6 sm:pl-3"
class="grid track-grid items-center gap-3 border-b border-line pr-4 pb-3 pl-2 text-sm text-fg-2 sm:gap-8 sm:pr-6 sm:pl-3"
style="--mf-track-cols:{gridColumns}; --mf-track-cols-mobile:{gridColumnsMobile}"
>
{#if index}
@@ -106,13 +106,14 @@
<span></span>
{/if}
</div>
<div class="mt-1 flex flex-col gap-0.5">
<div class="mt-1 flex flex-col gap-0.5" role="list">
{#each tracks as track, i (track.id)}
{@const active = player.current.id === track.id}
<div
role="listitem"
class="group grid track-grid items-center gap-3 rounded-control py-3 pr-4 pl-2 transition-colors sm:gap-8 sm:pr-6 sm:pl-3 {active
? 'bg-accent-tint'
? 'surface-active'
: 'hover:bg-hover'}"
style="--mf-track-cols:{gridColumns}; --mf-track-cols-mobile:{gridColumnsMobile}"
oncontextmenu={oncontextmenu ? (event) => oncontextmenu(event, track, i) : undefined}
@@ -122,7 +123,7 @@
type="button"
onclick={() => onPlay(i)}
aria-label={active && player.playing ? 'Pausar' : 'Reproducir'}
class="relative grid size-8 place-items-center rounded-tag text-xs text-muted tabular-nums"
class="relative grid size-8 place-items-center rounded-tag text-xs text-fg-2 tabular-nums"
>
{#if active}
<EqBars paused={!player.playing} />
@@ -138,6 +139,7 @@
tabindex="0"
use:pressable={() => onPlay(i)}
onclick={(event) => event.stopPropagation()}
onkeydown={(event) => event.stopPropagation()}
class="flex min-w-0 flex-1 text-left"
>
<MediaIdentity
@@ -156,7 +158,7 @@
</MediaIdentity>
</div>
{#each columns as column (column)}
<span class="hidden truncate text-center text-xs text-muted tabular-nums sm:block">
<span class="hidden truncate text-center text-xs text-fg-2 tabular-nums sm:block">
{#if column === 'plays'}
{Number(track.listensCount ?? 0)}
{:else if column === 'added' && track.addedAt}
@@ -166,7 +168,7 @@
{/if}
</span>
{/each}
<span class="truncate text-center text-xs text-muted tabular-nums">
<span class="truncate text-center text-xs text-fg-2 tabular-nums">
{fmtTime(Number(track.duration))}
</span>
{#if rowAction}
@@ -1,5 +1,5 @@
<script lang="ts">
import Modal from './Modal.svelte';
import Dialog from './Dialog.svelte';
import Button from '../primitives/Button.svelte';
interface Props {
@@ -14,13 +14,11 @@
let { open, onClose, title, description, confirmLabel, action }: Props = $props();
</script>
<Modal {open} {onClose} maxWidth="max-w-sm">
<h2 class="text-display-3 text-fg">{title}</h2>
<p class="mt-2.5 text-body text-fg-2">{description}</p>
<div class="mt-6 flex items-center justify-end gap-3">
<Dialog {open} {onClose} {title} {description}>
{#snippet actions()}
<Button variant="secondary" onclick={onClose}>Cancelar</Button>
<form method="POST" {action}>
<Button type="submit" variant="danger">{confirmLabel}</Button>
</form>
</div>
</Modal>
{/snippet}
</Dialog>
@@ -63,7 +63,7 @@
class="flex items-center gap-3 rounded-control px-2 py-2.5 text-sm font-medium text-fg-2 transition group-hover/addmenu:bg-hover"
>
<span class="flex-1 cursor-default">{playlistAction.label}</span>
<ChevronRight class="size-icon-sm shrink-0 text-fg-3" />
<ChevronRight class="size-icon-sm shrink-0 text-fg-2" />
</div>
<div
class="invisible absolute top-0 z-(--z-submenu) opacity-0 transition group-hover/addmenu:visible group-hover/addmenu:opacity-100 max-sm:top-full max-sm:right-0 max-sm:left-0 max-sm:px-0 max-sm:pt-1.5 {openLeft
@@ -75,7 +75,7 @@
>
<div class="max-h-80 overflow-y-auto">
{#if playlists.length === 0}
<p class="px-2 py-2.5 text-sm text-fg-3">Todavía no tienes playlists.</p>
<p class="px-2 py-2.5 text-sm text-fg-2">Todavía no tienes playlists.</p>
{/if}
{#each playlists as playlist (playlist.id)}
<form
@@ -0,0 +1,40 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import CircleAlert from '@lucide/svelte/icons/circle-alert';
import Modal from './Modal.svelte';
interface Props {
open: boolean;
onClose: () => void;
title: string;
description?: string;
tone?: 'neutral' | 'danger';
children?: Snippet;
actions?: Snippet;
}
let { open, onClose, title, description, tone = 'neutral', children, actions }: Props = $props();
</script>
<Modal
{open}
{onClose}
maxWidth="max-w-sm"
panelClass={tone === 'danger' ? 'glass-panel-danger' : ''}
>
{#if tone === 'danger'}
<span class="mb-4 grid size-10 place-items-center rounded-full bg-danger/12 text-danger-fg">
<CircleAlert class="size-icon-md" strokeWidth={2} />
</span>
{/if}
<h2 class="text-display-3 text-fg">{title}</h2>
{#if description}
<p class="mt-2.5 text-body text-fg-2">{description}</p>
{/if}
{@render children?.()}
{#if actions}
<div class="mt-6 flex flex-wrap items-center justify-end gap-3">
{@render actions()}
</div>
{/if}
</Modal>
@@ -0,0 +1,34 @@
<script lang="ts">
import Dialog from './Dialog.svelte';
import Button from '../primitives/Button.svelte';
import { dialog, type DialogAction, type DialogOptions } from '$lib/state/dialog.svelte';
// Keep the last dialog's content so the exit transition doesn't render an empty panel.
let last = $state<DialogOptions | null>(null);
$effect.pre(() => {
if (dialog.current) last = dialog.current;
});
const current = $derived(dialog.current ?? last);
const buttons: DialogAction[] = $derived(
current?.actions?.length ? current.actions : [{ label: 'Entendido' }]
);
</script>
<Dialog
open={dialog.current !== null}
onClose={() => dialog.close()}
title={current?.title ?? ''}
description={current?.description}
tone={current?.tone}
>
{#snippet actions()}
{#each buttons as action, i (action.label)}
<Button
variant={action.variant ?? (i === buttons.length - 1 ? 'primary' : 'secondary')}
onclick={() => dialog.run(action)}
>
{action.label}
</Button>
{/each}
{/snippet}
</Dialog>
@@ -12,7 +12,9 @@
</script>
<div
class="glass-panel animate-pop rounded-panel p-1.5 {border ? 'border border-hairline' : ''} {klass}"
class="glass-panel animate-pop rounded-panel p-1.5 {border
? 'border border-hairline'
: ''} {klass}"
{style}
>
{@render children()}
@@ -2,6 +2,9 @@
import type { Snippet } from 'svelte';
import X from '@lucide/svelte/icons/x';
import IconButton from '../primitives/IconButton.svelte';
import { fade } from 'svelte/transition';
import { expoOut } from 'svelte/easing';
import type { TransitionConfig } from 'svelte/transition';
interface Props {
open: boolean;
@@ -45,6 +48,15 @@
);
}
function pop(node: Element, { duration = 200 }: { duration?: number } = {}): TransitionConfig {
return {
duration,
easing: expoOut,
css: (t) =>
`opacity: ${t}; transform: scale(${0.98 + 0.02 * t}) translateY(${0.25 * (1 - t)}rem);`
};
}
function onWindowKeydown(event: KeyboardEvent) {
if (!open) return;
if (event.key === 'Escape') {
@@ -70,13 +82,15 @@
{#if open}
<div
class="animate-fade fixed inset-0 z-(--z-modal) grid place-items-center bg-scrim/70 p-4 backdrop-blur-sm"
transition:fade={{ duration: 200 }}
class="fixed inset-0 z-(--z-modal) grid place-items-center bg-scrim/70 p-4 backdrop-blur-sm"
role="presentation"
onclick={onBackdropClick}
>
<div
bind:this={panel}
class="glass-panel animate-pop max-h-[85dvh] w-full overflow-y-auto {maxWidth} rounded-panel p-5 outline-none sm:p-6 {panelClass}"
transition:pop={{ duration: 200 }}
class="glass-panel max-h-[85dvh] w-full overflow-y-auto {maxWidth} rounded-panel p-5 outline-none sm:p-6 {panelClass}"
role="dialog"
aria-modal="true"
aria-label={title}
@@ -86,7 +100,7 @@
<div class="flex items-center justify-between gap-4">
<div class="min-w-0">
{#if eyebrow}
<p class="mb-2.5 text-eyebrow text-muted">
<p class="mb-2.5 text-eyebrow text-fg-2">
{eyebrow}
</p>
{/if}
@@ -24,7 +24,7 @@
<a
href={target.url}
{onclick}
class="inline-flex items-center text-fg-3 transition-colors hover:text-accent-soft"
class="inline-flex items-center text-fg-2 transition-colors hover:text-accent-soft"
>
<span class="grid size-8 place-items-center">
<ArrowLeft class="size-icon-sm" />
@@ -29,20 +29,20 @@
}: Props = $props();
const sizes = {
sm: 'px-4 py-2.5 text-xs',
md: 'px-5 py-3 text-sm',
lg: 'px-6 py-3.5 text-sm'
sm: 'rounded-control px-4 py-2 text-xs',
md: 'rounded-btn-sm px-4.5 py-2.5 text-control-sm',
lg: 'rounded-btn px-6 py-3.5 text-sm'
};
const variants = {
primary: 'bg-cta-strong text-ink hover:brightness-95',
primary: 'bg-cta-strong text-ink hover:bg-cta-strong-hover',
secondary: 'bg-btn text-fg hover:bg-btn-hover',
accent: 'bg-accent-tint text-accent-soft hover:bg-accent-btn-hover',
accent: 'surface-active text-accent-soft hover:surface-active-hover',
danger: 'bg-danger text-on-art hover:brightness-110'
};
const classes = $derived(
`inline-flex items-center justify-center gap-2 rounded-control btn-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 font-medium btn-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>
@@ -15,9 +15,7 @@
href
? 'rounded-full border border-line-strong px-3.5 py-2 text-xs text-fg-2 transition-colors hover:bg-hover hover:text-fg'
: `flex items-center gap-2 rounded-full px-3.5 py-2 text-xs font-medium transition ${
selected
? 'bg-cta-strong text-ink'
: 'bg-surface-2 text-fg-2 hover:bg-surface-hover hover:text-fg'
selected ? 'bg-cta-strong text-ink' : 'bg-btn text-fg-2 hover:bg-btn-hover hover:text-fg'
}`
);
</script>
@@ -25,7 +23,7 @@
{#snippet content()}
{@render children()}
{#if count !== undefined}
<span class="tabular-nums {selected ? 'opacity-55' : 'text-muted'}">{count}</span>
<span class="tabular-nums {selected ? 'opacity-55' : 'text-fg-2'}">{count}</span>
{/if}
{/snippet}
@@ -20,7 +20,7 @@
{/if}
{@render children()}
{#if hint}
<div class="mt-2 flex items-center justify-between gap-3 text-xs text-muted">
<div class="mt-2 flex items-center justify-between gap-3 text-xs text-fg-2">
{@render hint()}
</div>
{/if}
@@ -41,7 +41,7 @@
};
const TEXT: Record<'muted' | 'subtle' | 'plain', string> = {
muted: 'text-muted hover:text-fg',
muted: 'text-fg-2 hover:text-fg',
subtle: 'text-fg-2 hover:text-fg',
plain: 'text-fg-2 hover:text-fg'
};
@@ -34,6 +34,6 @@
<div bind:this={sentinel} class="flex min-h-12 items-center justify-center py-8">
{#if loading}
<LoaderCircle class="size-5 animate-spin text-muted" />
<LoaderCircle class="size-5 animate-spin text-fg-2" />
{/if}
</div>
@@ -36,7 +36,7 @@
<div class="relative w-full">
{#if Icon}
<span class="pointer-events-none absolute inset-y-0 left-4 grid place-items-center text-muted">
<span class="pointer-events-none absolute inset-y-0 left-4 grid place-items-center text-fg-2">
<Icon class="size-icon-lg" />
</span>
{/if}
@@ -16,7 +16,7 @@
<div class="relative flex shrink-0 rounded-full bg-surface-2 p-1">
<div
class="absolute inset-y-1 left-1 rounded-full bg-fg transition-transform duration-300 ease-out"
class="absolute inset-y-1 left-1 rounded-full bg-fg transition-transform duration-200 ease-snappy"
style="width:calc((100% - 0.5rem) / {options.length}); transform:translateX(calc({index} * 100%))"
></div>
{#each options as option (option.value)}
@@ -26,7 +26,7 @@
class="relative z-(--z-raised) flex-1 px-4 py-1.5 text-center text-xs font-medium transition-colors {value ===
option.value
? 'text-ink'
: 'text-muted hover:text-fg-2'}"
: 'text-fg-2 hover:text-fg-2'}"
>
{option.label}
</button>