veza/scripts/align-8px-grid.py

212 lines
6.7 KiB
Python
Raw Normal View History

#!/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()