Files
Musify/web-player/src/lib/components/ui/ImageDropzone.svelte
T
Admin 1b6d7ecb42 web-player: final sweep and guardrails
Add scripts/check-tokens.mjs (npm run lint:tokens, wired into lint) as
a guardrail: fails if src/**/*.svelte reintroduces arbitrary values
outside the B.4 allowlist, factory rounded-*, h-N w-N pairs (should
be size-N), quarter-step fractional spacing, or literal hex/rgba()
colors.

Cleans up the repo until the script reports 0 violations: ~40 h-N w-N
pairs -> size-N (mostly icons, via a one-off script), ~26 quarter-step
fractional spacings rounded to the nearest integer (delta <=1px, e.g.
py-2.75->py-3, gap-1.75->gap-2), stray factory radii (rounded-sm/lg,
bare rounded) migrated to the matching tokens, and two
tracking-[-0.02em] that matched the existing tracking-display token
exactly but had never been migrated. Removes the
stroke-thin/regular/bold recipes (added earlier, zero consumers) —
decision made, not blocking: strokeWidth={n} (13 values, across
almost every component) isn't migrated to CSS because there's no way
to verify without Docker that lucide-svelte honors stroke-width by
class instead of the prop, and a regression there would be silent
across the whole app; the script documents this gap explicitly
instead of pretending full coverage.

Orphan detector: 0 components with no importers. Removes
docs/design-system-refactor-plan.md (earlier plan, superseded by
frontend-refactor-plan.md). web-player/CLAUDE.md gains a "Styles"
section with the B.4 allowlist and the base components to reuse.
Recalculates the baseline table (A.4) and logs it: 61->47 components,
44->11 arbitrary values (the remaining 11 on the allowlist), 13->8
distinct radii, 0 lint:tokens violations.

Verified with npm run check (0 errors), npm run lint:tokens (0
violations) and npm run build. Smoke-tested /login with no console
errors attributable to this change.
2026-09-21 15:05:07 +02:00

74 lines
1.8 KiB
Svelte

<script lang="ts">
import ImageIcon from '@lucide/svelte/icons/image';
import type { LucideIcon } from '@lucide/svelte';
interface Props {
name: string;
fallbackUrl?: string;
required?: boolean;
icon?: LucideIcon;
gradient?: boolean;
size?: 'default' | 'hero';
onselect?: (file: File) => void;
class?: string;
}
let {
name,
fallbackUrl,
required = false,
icon: Icon = ImageIcon,
gradient = false,
size = 'default',
onselect,
class: klass = ''
}: Props = $props();
const SIZE: Record<'default' | 'hero', string> = {
default: 'size-40 sm:size-52 rounded-control',
hero: 'size-cover-hero rounded-art-lg'
};
let preview = $state('');
let fallbackFailed = $state(false);
const hasImage = $derived(preview !== '' || (!!fallbackUrl && !fallbackFailed));
function onInput(event: Event) {
const file = (event.currentTarget as HTMLInputElement).files?.[0];
if (!file) return;
preview = URL.createObjectURL(file);
onselect?.(file);
}
</script>
<label
class="relative grid cursor-pointer place-items-center overflow-hidden transition-colors {gradient
? ''
: 'border border-line bg-surface hover:border-accent/50'} {SIZE[size]} {klass}"
style={gradient && !hasImage ? 'background:var(--mf-cover-grad)' : undefined}
>
<input
type="file"
{name}
accept="image/*"
{required}
onchange={onInput}
class="absolute inset-0 cursor-pointer opacity-0"
aria-label="Seleccionar portada"
/>
{#if preview !== ''}
<img src={preview} alt="Portada" class="h-full w-full object-cover" />
{:else if fallbackUrl && !fallbackFailed}
<img
src={fallbackUrl}
alt="Portada actual"
loading="lazy"
onerror={() => (fallbackFailed = true)}
class="h-full w-full object-cover"
/>
{:else}
<Icon class="size-icon-xl {gradient ? 'text-on-art/70' : 'text-muted'}" strokeWidth={1.3} />
{/if}
</label>