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.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env node
|
||||
// Guardrail contra la reintroducción de lo que P0-P14 limpió
|
||||
// (ver docs/frontend-refactor-plan.md, secciones B.4 y P14). Escanea
|
||||
// src/**/*.svelte en busca de valores arbitrarios y utilidades de Tailwind
|
||||
// que deberían ser tokens del sistema de diseño (src/routes/layout.css).
|
||||
//
|
||||
// Gaps conocidos, no cubiertos aquí a propósito:
|
||||
// - `strokeWidth={n}` en iconos: hay ~13 valores distintos repartidos por
|
||||
// casi todos los componentes. Migrarlo a las recetas `stroke-*` requeriría
|
||||
// verificar que lucide-svelte respeta `stroke-width` por CSS en vez del
|
||||
// atributo del componente, y no hay forma de comprobarlo visualmente en
|
||||
// esta sesión (sin Docker). Se deja fuera para no arriesgar una regresión
|
||||
// silenciosa en todos los iconos de la app.
|
||||
// - `text-base` sin receta: solo queda un caso real (Queue.svelte, título
|
||||
// "En cola", 16px) sin equivalente en la escala text-display-*. Crear una
|
||||
// receta nueva para un único consumidor sería una abstracción prematura;
|
||||
// se acepta como excepción documentada en vez de forzarlo a un tamaño que
|
||||
// no le corresponde.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const files = execSync('git ls-files "src/**/*.svelte"', { encoding: 'utf8' })
|
||||
.split('\n')
|
||||
.filter(Boolean);
|
||||
|
||||
// B.4 — arbitrarios permitidos tras el refactor.
|
||||
const ARBITRARY_WHITELIST = [
|
||||
/^text-\[clamp\([^)]+\)\]$/, // tipografía fluida (login, Logo)
|
||||
/^leading-\[[\d.]+\]$/, // leading ajustado del display gigante de login
|
||||
/^grid-cols-\[auto_1fr\]$/, // upload: portada + campos
|
||||
/^max-h-\[85dvh\]$/, // Modal
|
||||
/^my-\[\d+d?vh\]$/, // +error.svelte: centrado vertical de página de error
|
||||
/^py-\[\d+d?vh\]$/, // Explorar: centrado vertical de "sin resultados"
|
||||
/^transition-\[filter\]$/, // Explorar: genre-tile hover
|
||||
/^bg-\[image:var\(--mf-[\w-]+\)\]$/ // gradientes dinámicos por tema (Artwork, Logo, liked)
|
||||
];
|
||||
|
||||
// Radios semánticos de B.1.1 — cualquier otro rounded-* de Tailwind (o el
|
||||
// `rounded` a secas de fábrica) es una regresión. `rounded-full` y los
|
||||
// tokens (`rounded-tag/thumb/control/art/art-lg/panel/panel-lg`) están bien:
|
||||
// el lookahead exige que no venga más texto de clase pegado con `-`.
|
||||
const RAW_ROUNDED = /\brounded(-(none|xs|sm|md|lg|xl|2xl|3xl))?(?![\w-])/g;
|
||||
|
||||
const PAIRED_HW = /\b(?:([a-z0-9-]+:))?h-([\d.]+)\s+\1w-\2\b/g;
|
||||
const QUARTER_STEP_SPACING = /\b[a-z-]+-\d+\.(25|75)\b/g;
|
||||
const HEX_COLOR = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const RGBA_LITERAL = /\brgba?\(/g;
|
||||
|
||||
let violations = [];
|
||||
|
||||
for (const file of files) {
|
||||
const content = readFileSync(file, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
lines.forEach((line, i) => {
|
||||
const lineNo = i + 1;
|
||||
|
||||
for (const match of line.matchAll(/[a-zA-Z][a-zA-Z0-9-]*-\[[^\]]+\]/g)) {
|
||||
if (!ARBITRARY_WHITELIST.some((re) => re.test(match[0]))) {
|
||||
violations.push(`${file}:${lineNo} — arbitrario fuera de la lista blanca: \`${match[0]}\``);
|
||||
}
|
||||
}
|
||||
|
||||
for (const match of line.matchAll(RAW_ROUNDED)) {
|
||||
violations.push(`${file}:${lineNo} — radio de Tailwind sin tokenizar: \`${match[0]}\``);
|
||||
}
|
||||
|
||||
for (const match of line.matchAll(PAIRED_HW)) {
|
||||
violations.push(`${file}:${lineNo} — usa \`size-${match[2]}\` en vez de \`${match[0]}\``);
|
||||
}
|
||||
|
||||
for (const match of line.matchAll(QUARTER_STEP_SPACING)) {
|
||||
violations.push(
|
||||
`${file}:${lineNo} — espaciado fraccionario en cuartos: \`${match[0]}\` (redondear al entero más cercano)`
|
||||
);
|
||||
}
|
||||
|
||||
for (const match of line.matchAll(HEX_COLOR)) {
|
||||
violations.push(
|
||||
`${file}:${lineNo} — color hex literal: \`${match[0]}\` (usa un token --mf-*)`
|
||||
);
|
||||
}
|
||||
|
||||
for (const match of line.matchAll(RGBA_LITERAL)) {
|
||||
violations.push(
|
||||
`${file}:${lineNo} — color rgba() literal: \`${match[0]}\` (usa un token --mf-*)`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error(`\n${violations.length} violación(es) del sistema de diseño:\n`);
|
||||
for (const v of violations) console.error(` ${v}`);
|
||||
console.error('\nVer docs/frontend-refactor-plan.md, sección B.4.\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('check-tokens: sin violaciones.');
|
||||
Reference in New Issue
Block a user