#!/usr/bin/env python3 """ Script to align all spacing to 8px grid for Action 11.2.1.3 This script: 1. Finds all non-8px-aligned spacing values (1, 3, 5, 10, 20) 2. Replaces them with nearest 8px-aligned values 3. Preserves necessary fine-tuning (4px values in specific contexts) """ import os import re import sys from pathlib import Path from typing import List, Tuple # Non-8px-aligned spacing values and their replacements # Note: We're conservative with 4px (spacing-1) values as they might be intentional fine-tuning SPACING_REPLACEMENTS = { # 12px (spacing-3) → 16px (better spacing) 'gap-3': 'gap-4', # 12px → 16px 'p-3': 'p-4', # 12px → 16px 'm-3': 'm-4', # 12px → 16px 'px-3': 'px-4', 'py-3': 'py-4', 'mx-3': 'mx-4', 'my-3': 'my-4', 'space-x-3': 'space-x-4', 'space-y-3': 'space-y-4', # 20px (spacing-5) → 16px or 24px (prefer 24px for better spacing) 'gap-5': 'gap-6', # 20px → 24px 'p-5': 'p-6', # 20px → 24px 'm-5': 'm-6', # 20px → 24px 'px-5': 'px-6', 'py-5': 'py-6', 'mx-5': 'mx-6', 'my-5': 'my-6', 'space-x-5': 'space-x-6', 'space-y-5': 'space-y-6', # 40px (spacing-10) → 32px or 48px (prefer 48px for better spacing) 'gap-10': 'gap-12', # 40px → 48px 'p-10': 'p-12', # 40px → 48px 'm-10': 'm-12', # 40px → 48px 'px-10': 'px-12', 'py-10': 'py-12', 'mx-10': 'mx-12', 'my-10': 'my-12', 'space-x-10': 'space-x-12', 'space-y-10': 'space-y-12', # 80px (spacing-20) → 64px or 96px (prefer 96px for better spacing) 'gap-20': 'gap-24', # 80px → 96px 'p-20': 'p-24', # 80px → 96px 'm-20': 'm-24', # 80px → 96px 'px-20': 'px-24', 'py-20': 'py-24', 'mx-20': 'mx-24', 'my-20': 'my-24', 'space-x-20': 'space-x-24', 'space-y-20': 'space-y-24', } # Patterns to preserve (contexts where non-8px spacing might be intentional) PRESERVE_PATTERNS = [ # Test files r'\.test\.', r'\.spec\.', # Documentation r'\.md', # Responsive breakpoints (sm:, md:, lg:) - these are intentional r'sm:gap-[135]|md:gap-[135]|lg:gap-[135]', r'sm:p-[135]|md:p-[135]|lg:p-[135]', r'sm:m-[135]|md:m-[135]|lg:m-[135]', # Arbitrary values that might be intentional r'gap-\[|p-\[|m-\[', ] # Files/directories to skip SKIP_PATTERNS = [ 'node_modules', '.git', '.build', 'dist', 'coverage', 'test-reports', 'EXHAUSTIVE_TODO_LIST.md', 'GRID_SYSTEM.md', 'SPACING_AUDIT_REPORT.md', 'SPACING_GUIDE.md', 'design-tokens.css', # This file defines the spacing scale ] def should_preserve(line: str, file_path: str) -> bool: """Check if this line should be preserved""" # Skip test and documentation files if any(pattern in file_path for pattern in SKIP_PATTERNS): return True # Check preserve patterns for pattern in PRESERVE_PATTERNS: if re.search(pattern, line, re.IGNORECASE): return True return False def replace_spacing(file_path: Path, dry_run: bool = False) -> Tuple[int, int]: """Replace non-8px spacing in a file""" replaced = 0 preserved = 0 try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() original_content = content lines = content.split('\n') new_lines = [] for line_num, line in enumerate(lines, 1): original_line = line # Check if should preserve if should_preserve(line, str(file_path)): # Count preserved instances for old in SPACING_REPLACEMENTS.keys(): if old in line: preserved += 1 break new_lines.append(line) continue # Replace non-8px spacing line_replaced = False for old, new in SPACING_REPLACEMENTS.items(): # Use word boundaries to avoid partial matches pattern = r'\b' + re.escape(old) + r'\b' if re.search(pattern, line): line = re.sub(pattern, new, line) if not line_replaced: replaced += 1 line_replaced = True new_lines.append(line) new_content = '\n'.join(new_lines) if not dry_run and new_content != original_content: with open(file_path, 'w', encoding='utf-8') as f: f.write(new_content) return replaced, preserved except Exception as e: print(f"Error processing {file_path}: {e}", file=sys.stderr) return 0, 0 def main(): """Main function""" import argparse parser = argparse.ArgumentParser(description='Align spacing to 8px grid') parser.add_argument('--dry-run', action='store_true', help='Show what would be changed without making changes') parser.add_argument('--path', default='apps/web/src', help='Path to search (default: apps/web/src)') args = parser.parse_args() base_path = Path(args.path) if not base_path.exists(): print(f"Error: Path {base_path} does not exist", file=sys.stderr) sys.exit(1) # Find all TypeScript/TSX files tsx_files = [] for ext in ['*.tsx', '*.ts']: tsx_files.extend(base_path.rglob(ext)) # Filter out skipped files files_to_process = [] for file_path in tsx_files: if not any(pattern in str(file_path) for pattern in SKIP_PATTERNS): files_to_process.append(file_path) print(f"Found {len(files_to_process)} files to process") if args.dry_run: print("DRY RUN MODE - No files will be modified\n") total_replaced = 0 total_preserved = 0 files_modified = [] for file_path in sorted(files_to_process): replaced, preserved = replace_spacing(file_path, dry_run=args.dry_run) if replaced > 0: total_replaced += replaced total_preserved += preserved files_modified.append((file_path, replaced, preserved)) status = "[DRY RUN] Would modify" if args.dry_run else "Modified" print(f"{status}: {file_path} ({replaced} replaced, {preserved} preserved)") print(f"\n{'='*60}") print(f"Total files {'would be ' if args.dry_run else ''}modified: {len(files_modified)}") print(f"Total instances replaced: {total_replaced}") print(f"Total instances preserved: {total_preserved}") print(f"{'='*60}") if args.dry_run and len(files_modified) > 0: print("\nRun without --dry-run to apply changes") if __name__ == '__main__': main()