Plan UI premium 6–8 semaines (design system, shell, Storybook, a11y): - Design system: DESIGN_TOKENS.md, APP_SHELL.md, FULL_LAYOUT_PAGE.md. Single source for layout/shell (index.css), shadows (design-system.css), durations/easing. - Tokens: shadow-cover-depth, shadow-gold-glow, shadow-fab-glow; layout max-height (max-h-layout-drawer, max-h-layout-panel, max-h-layout-list). All duration-200/300/500 replaced by --duration-fast/normal/slow. Arbitrary shadows replaced by token classes. - Shell & player: Sidebar, Header, GlobalPlayer, MiniPlayer, PlayerQueue, PlayerControls, AudioPlayer use tokens; focus-visible on Sidebar, PlayerQueue, DropdownMenuTrigger/Item, TabsTrigger. Typography: text-[10px]/[9px] → text-xs where applicable. - ESLint: no-restricted-syntax (warn) for w-/h-/rounded-/shadow-/text-/spacing arbitrary. - Scripts: report-arbitrary-values.mjs, capture/compare/generate visual; visual-complete.spec.ts. - Stories full layout: Dashboard, Playlists, Library, Settings, Profile in DashboardLayout.stories. - .cursorrules + README: DESIGN_TOKENS, APP_SHELL, visual commands, no arbitrary without justification. - apps/web/.gitignore: e2e test artifacts (test-results-visual, playwright-report-visual). Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
3.3 KiB
JavaScript
107 lines
3.3 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Rapport des valeurs arbitraires Tailwind (w-[...], h-[...], gap-[...], etc.)
|
|
* pour prioriser les migrations vers le design system.
|
|
* Usage: node scripts/report-arbitrary-values.mjs [--json] [--dir src/components]
|
|
* Sortie: rapport (fichier, ligne, pattern) — pas de remplacement automatique.
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const ARBITRARY_PATTERNS = [
|
|
{ name: 'width', regex: /(?:^|\s)(w-|min-w-|max-w-)\[[^\]]+\]/g },
|
|
{ name: 'height', regex: /(?:^|\s)(h-|min-h-|max-h-)\[[^\]]+\]/g },
|
|
{ name: 'gap', regex: /(?:^|\s)gap-\[[^\]]+\]/g },
|
|
{ name: 'padding', regex: /(?:^|\s)(p-|px-|py-|pt-|pb-|pl-|pr-)\[[^\]]+\]/g },
|
|
{ name: 'margin', regex: /(?:^|\s)(m-|mx-|my-|mt-|mb-|ml-|mr-)\[[^\]]+\]/g },
|
|
{ name: 'space', regex: /(?:^|\s)space-[xy]-\[[^\]]+\]/g },
|
|
{ name: 'rounded', regex: /(?:^|\s)rounded-\[[^\]]+\]/g },
|
|
{ name: 'shadow', regex: /(?:^|\s)shadow-\[[^\]]+\]/g },
|
|
];
|
|
|
|
const DEFAULT_DIRS = ['src/components', 'src/features'];
|
|
const IGNORE_DIRS = ['node_modules', 'dist', '.storybook', 'dist_verification'];
|
|
const EXTENSIONS = new Set(['.tsx', '.jsx', '.ts', '.js']);
|
|
|
|
function parseArgs() {
|
|
const args = process.argv.slice(2);
|
|
let dirs = DEFAULT_DIRS;
|
|
let json = false;
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--json') json = true;
|
|
else if (args[i] === '--dir' && args[i + 1]) {
|
|
dirs = [args[++i]];
|
|
}
|
|
}
|
|
return { dirs, json };
|
|
}
|
|
|
|
function* walkFiles(dir, base = dir) {
|
|
if (!fs.existsSync(dir)) return;
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
for (const e of entries) {
|
|
const full = path.join(dir, e.name);
|
|
const rel = path.relative(base, full);
|
|
if (e.isDirectory()) {
|
|
if (!IGNORE_DIRS.includes(e.name)) yield* walkFiles(full, base);
|
|
} else if (EXTENSIONS.has(path.extname(e.name))) {
|
|
yield { full, rel };
|
|
}
|
|
}
|
|
}
|
|
|
|
function scanFile(filePath) {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const lines = content.split('\n');
|
|
const hits = [];
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
for (const { name, regex } of ARBITRARY_PATTERNS) {
|
|
regex.lastIndex = 0;
|
|
let m;
|
|
while ((m = regex.exec(line)) !== null) {
|
|
hits.push({
|
|
pattern: name,
|
|
match: m[0].trim(),
|
|
line: i + 1,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return hits;
|
|
}
|
|
|
|
function main() {
|
|
const { dirs, json } = parseArgs();
|
|
const cwd = process.cwd();
|
|
const report = [];
|
|
|
|
for (const dir of dirs) {
|
|
const absDir = path.isAbsolute(dir) ? dir : path.join(cwd, dir);
|
|
for (const { full, rel } of walkFiles(absDir)) {
|
|
const hits = scanFile(full);
|
|
if (hits.length > 0) {
|
|
report.push({ file: rel, hits });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (json) {
|
|
console.log(JSON.stringify(report, null, 2));
|
|
return;
|
|
}
|
|
|
|
console.log('# Rapport des valeurs arbitraires Tailwind\n');
|
|
console.log('Prioriser les migrations vers tokens / scale (voir docs/DESIGN_TOKENS.md).\n');
|
|
for (const { file, hits } of report) {
|
|
console.log(`## ${file}`);
|
|
for (const { pattern, match, line } of hits) {
|
|
console.log(` ${line}: [${pattern}] ${match}`);
|
|
}
|
|
console.log('');
|
|
}
|
|
console.log(`Total: ${report.length} fichiers, ${report.reduce((s, r) => s + r.hits.length, 0)} occurrences.`);
|
|
}
|
|
|
|
main();
|