Añade scripts/check-tokens.mjs (npm run lint:tokens, incorporado a
lint) como guardrail: falla si src/**/*.svelte reintroduce valores
arbitrarios fuera de la lista blanca de B.4, rounded-* de fábrica,
pares h-N w-N (deberían ser size-N), espaciado fraccionario en
cuartos, o colores hex/rgba() literales.
Se limpia el repo hasta dejar el script en 0 violaciones: ~40 pares
h-N w-N → size-N (sobre todo iconos, vía script puntual), ~26
espaciados fraccionarios en cuartos redondeados al entero más cercano
(Δ≤1px, p. ej. py-2.75→py-3, gap-1.75→gap-2), radios de fábrica
sueltos (rounded-sm/lg, rounded a secas) migrados a los tokens
correspondientes, y dos tracking-[-0.02em] que coincidían
exactamente con el token tracking-display ya existente pero nunca se
habían migrado. Se retiran las recetas stroke-thin/regular/bold
(creadas en P1, cero consumidores) — decisión tomada, sin bloquear:
no se migra strokeWidth={n} (13 valores, en casi todos los
componentes) a CSS porque no se puede verificar sin Docker que
lucide-svelte respete stroke-width por clase en vez del prop, y una
regresión ahí sería silenciosa en toda la app; el script documenta
este gap explícitamente en vez de fingir cobertura completa.
Detector de huérfanos: 0 componentes sin importadores. Se borra
docs/design-system-refactor-plan.md (plan anterior, superseded por
frontend-refactor-plan.md). web-player/CLAUDE.md gana una sección
"Estilos" con la lista blanca B.4 y los componentes base a reutilizar.
Se recalcula la tabla de línea base (A.4) y se anota en el Registro
(D.1): 61→47 componentes, 44→11 arbitrarios (los 11 restantes en
lista blanca), 13→8 radios distintos, 0 violaciones de lint:tokens.
Verificado con npm run check (0 errores), npm run lint:tokens (0
violaciones) y npm run build. Smoke test de /login sin errores de
consola achacables al cambio (Docker no disponible en esta sesión).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
74 lines
1.8 KiB
Svelte
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>
|