Align library styles with the design system

Use type recipes and Button danger variant, add danger tint, dropzone and cover-fill tokens, give SearchInput a size prop so the top bar keeps its larger search, show queue details inline on mobile, derive upload limits from constants and document the card hover style.
This commit is contained in:
2026-09-25 01:20:32 +02:00
parent 074ea7bf26
commit ebe9769d4e
12 changed files with 54 additions and 11 deletions
@@ -77,6 +77,7 @@
<SearchInput
bind:value={query}
placeholder="Filtrar por título o artista"
size="sm"
class="w-full sm:max-w-search sm:flex-1"
/>
<ChipGroup label="Filtrar por estado" options={filterOptions} bind:value={filter} />
@@ -6,6 +6,7 @@
summarizeQueue,
toQueuedUpload,
UPLOAD_FORMATS,
uploadLimitsLabel,
type QueuedUpload,
type Visibility
} from '$lib/data/uploadQueue';
@@ -69,7 +70,7 @@
<FileDropzone
accept={ACCEPT}
title="Arrastra tus archivos aquí"
hint="WAV, FLAC, AIFF o MP3 · hasta 500 MB por archivo · puedes soltar carpetas enteras"
hint={uploadLimitsLabel()}
inputLabel="Elegir archivos de audio"
onfiles={enqueue}
readDrop={filesFromDrop}
@@ -88,7 +89,7 @@
<section class="min-w-0 rounded-panel bg-surface p-5 sm:p-6">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h2 class="font-display text-xl font-semibold tracking-display text-fg">Cola de subida</h2>
<h2 class="text-display-3 text-fg">Cola de subida</h2>
<p class="mt-1 text-xs text-fg-2">
{queue.length > 0 ? queueMeta : 'Todavía no hay archivos'}
</p>
@@ -57,6 +57,17 @@
onTitle,
class: klass = ''
}: Props = $props();
function details(item: QueuedUpload): string {
return [
genresLabel,
visibilityLabel,
formatSize(item.file.size),
item.duration === null ? '' : fmtTime(item.duration)
]
.filter((part) => part !== '' && part !== '-')
.join(' · ');
}
</script>
<DataGrid
@@ -82,6 +93,7 @@
class="w-full truncate rounded-tag bg-transparent text-sm text-fg transition-colors hover:bg-surface focus:bg-surface-2 focus:outline-none"
/>
<p class="truncate text-xs text-fg-2">{item.file.name}</p>
<p class="truncate text-xs text-fg-2 sm:hidden">{details(item)}</p>
</div>
</div>
{:else if column.id === 'genres'}
@@ -59,11 +59,11 @@
</script>
<aside class="flex flex-col gap-6 {klass}">
<p class="text-xs font-medium tracking-eyebrow text-fg-2 uppercase">Aplicar a esta subida</p>
<p class="text-eyebrow text-fg-2">Aplicar a esta subida</p>
<div class="grid gap-8 sm:grid-cols-[auto_1fr]">
<Field label="Portada">
<div class="w-full max-w-44">
<div class="w-full max-w-cover-fill">
<ImageDropzone
name="cover"
icon={ImageIcon}
@@ -75,6 +75,7 @@
icon={coverIcon}
gradient
size="fill"
class="max-w-cover-fill lg:max-w-none"
/>
{#if coverHint}
<p class="text-xs text-fg-2">{coverHint}</p>
@@ -70,14 +70,14 @@
ondragover={onOver}
ondragleave={onLeave}
ondrop={onDrop}
class="flex min-h-64 flex-col items-center justify-center rounded-panel border border-dashed p-6 text-center transition sm:p-10 {dragging
class="flex min-h-dropzone flex-col items-center justify-center rounded-panel border border-dashed p-6 text-center transition sm:p-10 {dragging
? 'border-accent/60 bg-accent-tint'
: 'border-line-strong bg-surface'} {klass}"
>
<span class="grid size-14 place-items-center rounded-full bg-surface-2 text-fg-2">
<UploadCloud class="size-icon-xl" />
</span>
<h2 class="mt-5 font-display text-xl font-semibold tracking-display text-fg">{title}</h2>
<h2 class="mt-5 text-display-3 text-fg">{title}</h2>
{#if hint}
<p class="mt-2 text-xs text-fg-2">{hint}</p>
{/if}
@@ -10,7 +10,7 @@
const TONE = {
success: 'bg-accent-tint text-accent-soft',
neutral: 'bg-btn text-fg-2',
danger: 'bg-danger/15 text-danger-fg'
danger: 'bg-danger-tint text-danger-fg'
};
</script>
@@ -6,6 +6,7 @@
value?: string;
placeholder?: string;
label?: string;
size?: 'sm' | 'md';
oninput?: () => void;
onclear?: () => void;
class?: string;
@@ -15,11 +16,17 @@
value = $bindable(''),
placeholder = 'Buscar',
label = placeholder,
size = 'md',
oninput,
onclear,
class: klass = ''
}: Props = $props();
const SIZE = {
sm: { text: 'text-xs', icon: 'size-icon-xs' },
md: { text: 'text-sm', icon: 'size-icon-sm' }
};
let input: HTMLInputElement | undefined = $state();
function clear() {
@@ -32,7 +39,7 @@
<label
class="flex h-9.5 min-w-0 items-center gap-2.5 rounded-control bg-surface-2 px-3.5 transition focus-within:bg-surface-hover {klass}"
>
<Search class="size-icon-xs shrink-0 text-fg-2" />
<Search class="{SIZE[size].icon} shrink-0 text-fg-2" />
<input
type="text"
bind:this={input}
@@ -40,7 +47,8 @@
{oninput}
{placeholder}
aria-label={label}
class="w-full min-w-0 bg-transparent text-xs text-fg placeholder:text-fg-2 focus:outline-none"
class="w-full min-w-0 bg-transparent {SIZE[size]
.text} text-fg placeholder:text-fg-2 focus:outline-none"
/>
{#if value}
<button
@@ -4,6 +4,7 @@ import {
titleFromFileName,
toQueuedUpload,
uploadError,
uploadLimitsLabel,
UPLOAD_MAX_BYTES
} from '$lib/data/uploadQueue';
@@ -43,3 +44,11 @@ describe('summarizeQueue', () => {
expect(summarizeQueue(items)).toEqual({ ready: 2, failed: 1 });
});
});
describe('uploadLimitsLabel', () => {
it('lists the formats and the size limit', () => {
expect(uploadLimitsLabel()).toBe(
'WAV, FLAC, AIFF o MP3 · hasta 500 MB por archivo · puedes soltar carpetas enteras'
);
});
});
+9 -1
View File
@@ -1,6 +1,14 @@
export const UPLOAD_MAX_BYTES = 500 * 1024 * 1024;
export const UPLOAD_FORMATS = ['wav', 'flac', 'aiff', 'aif', 'mp3'];
const UPLOAD_MAX_MB = Math.round(UPLOAD_MAX_BYTES / (1024 * 1024));
export function uploadLimitsLabel(): string {
const names = UPLOAD_FORMATS.filter((ext) => ext !== 'aif').map((ext) => ext.toUpperCase());
const list = `${names.slice(0, -1).join(', ')} o ${names[names.length - 1]}`;
return `${list} · hasta ${UPLOAD_MAX_MB} MB por archivo · puedes soltar carpetas enteras`;
}
export type Visibility = 'private' | 'public';
export interface QueuedUpload {
@@ -30,7 +38,7 @@ export function titleFromFileName(name: string): string {
export function uploadError(file: File): string | null {
if (!UPLOAD_FORMATS.includes(fileExtension(file.name))) return 'Formato no compatible';
if (file.size > UPLOAD_MAX_BYTES) return 'Supera 500 MB';
if (file.size > UPLOAD_MAX_BYTES) return `Supera ${UPLOAD_MAX_MB} MB`;
if (file.size === 0) return 'Archivo vacío';
return null;
}