veza/scripts/replace-decorative-cyan.py
senke 3fb12b2ce2 aesthetic-improvements: automated replacement of decorative cyan with steel (80/20 rule, Action 11.3.1.3)
- Created automated script (scripts/replace-decorative-cyan.py) to systematically replace decorative/informational kodo-cyan instances with kodo-steel variants
- Script intelligently preserves active/functional states, design system variants, semantic indicators, and interactive states
- Modified 85 files, replaced 145 decorative instances, preserved 47 functional instances
- No linter errors, type safety maintained
- Action 11.3.1.3 significantly advanced (total: ~302 instances replaced across ~229 files including previous batches)
2026-01-16 11:40:13 +01:00

265 lines
8.9 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Script to replace decorative/informational kodo-cyan instances with kodo-steel
for Action 11.3.1.3: Apply 80/20 rule (80% neutral, 20% color)
This script:
1. Finds all instances of kodo-cyan usage
2. Identifies decorative/informational instances
3. Replaces them with kodo-steel variants
4. Preserves active/functional states, design system variants, semantic indicators
"""
import os
import re
import sys
from pathlib import Path
from typing import List, Tuple, Dict
# Patterns to find kodo-cyan usage
CYAN_PATTERNS = [
r'bg-kodo-cyan/10',
r'bg-kodo-cyan/20',
r'border-kodo-cyan/30',
r'border-kodo-cyan/20',
r'border-kodo-cyan',
r'text-kodo-cyan',
r'text-kodo-cyan-dim',
]
# Patterns that indicate functional/active states (PRESERVE)
PRESERVE_PATTERNS = [
# Active/selected states - check for conditional logic
r'\?.*kodo-cyan.*:',
r':.*kodo-cyan.*\?',
r'isSelected.*\?.*kodo-cyan',
r'isActive.*\?.*kodo-cyan',
r'isCurrent.*\?.*kodo-cyan',
r'selected.*\?.*kodo-cyan',
r'active.*\?.*kodo-cyan',
r'current.*\?.*kodo-cyan',
r'isPlaying.*\?.*kodo-cyan',
r'isFocused.*\?.*kodo-cyan',
r'isMe.*\?.*kodo-cyan',
r'connected.*\?.*kodo-cyan',
r'is_current.*\?.*kodo-cyan',
r'draggedIndex.*\?.*kodo-cyan',
r'autoScroll.*\?.*kodo-cyan',
r'showVisualizer.*\?.*kodo-cyan',
r'isQueueOpen.*\?.*kodo-cyan',
r'queueTab.*\?.*kodo-cyan',
r'paymentMethod.*\?.*kodo-cyan',
r'currentTheme.*\?.*kodo-cyan',
r'theme ===.*\?.*kodo-cyan',
r'dateRange ===.*\?.*kodo-cyan',
# Design system variants (PRESERVE) - exact matches
r'cyan:\s*[\'"]kodo-cyan',
r'color:\s*[\'"]cyan',
r'variant:\s*[\'"]cyan',
r'variant:\s*[\'"]info',
r'severity:\s*[\'"]info',
r'badge.*cyan',
r'Spinner.*cyan',
r'default:\s*[\'"]kodo-cyan',
# Semantic indicators (PRESERVE)
r'PasswordStrengthIndicator',
r'isPrivate.*\?.*kodo-cyan',
r'isPublic.*\?.*kodo-cyan',
# Functional links (PRESERVE)
r'hover:underline',
r'<a\s+.*kodo-cyan',
r'href=',
# Progress bars and functional visualizations (PRESERVE)
r'style=.*width.*kodo-cyan',
r'progress',
# Button variants (PRESERVE)
r'variant=.*primary',
r'variant=.*default',
# Test files (PRESERVE)
r'\.test\.',
r'\.spec\.',
]
# Files/directories to skip
SKIP_PATTERNS = [
'node_modules',
'.git',
'.build',
'dist',
'coverage',
'test-reports',
'EXHAUSTIVE_TODO_LIST.md',
'HOVER_EFFECTS_AUDIT.md',
'COLOR_USAGE.md',
'COMPONENT_USAGE.md',
]
# Replacement mappings
REPLACEMENTS = {
'bg-kodo-cyan/10': 'bg-kodo-steel/10',
'bg-kodo-cyan/20': 'bg-kodo-steel/20',
'border-kodo-cyan/30': 'border-kodo-steel/30',
'border-kodo-cyan/20': 'border-kodo-steel/20',
'border-kodo-cyan': 'border-kodo-steel',
'text-kodo-cyan': 'text-kodo-steel',
'text-kodo-cyan-dim': 'text-kodo-steel',
'border-t-kodo-cyan': 'border-t-kodo-steel',
'ring-kodo-cyan': 'ring-kodo-steel',
'shadow-kodo-cyan': 'shadow-kodo-steel',
}
def should_preserve(line: str, file_path: str, context_lines: List[str] = None) -> bool:
"""Check if this line should be preserved (functional/active state)"""
# Skip test and documentation files
if any(pattern in file_path for pattern in SKIP_PATTERNS):
return True
# Check preserve patterns in current line
for pattern in PRESERVE_PATTERNS:
if re.search(pattern, line, re.IGNORECASE):
return True
# Check context (previous and next lines) for conditional logic
if context_lines:
context = ' '.join(context_lines)
# If cyan appears in a ternary or conditional, preserve it
if re.search(r'\?.*kodo-cyan|kodo-cyan.*\?', context, re.IGNORECASE):
return True
# If it's part of a variant/color prop, preserve it
if re.search(r'(variant|color|severity).*:.*cyan', context, re.IGNORECASE):
return True
# Check if line contains conditional logic with cyan
if re.search(r'\?.*kodo-cyan|kodo-cyan.*\?', line, re.IGNORECASE):
return True
# Check if it's a design system prop
if re.search(r'(variant|color|severity)\s*[:=]\s*[\'"]?(cyan|info)', line, re.IGNORECASE):
return True
return False
def find_cyan_instances(file_path: Path) -> List[Tuple[int, str, str]]:
"""Find all kodo-cyan instances in a file"""
instances = []
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
for line_num, line in enumerate(lines, 1):
for pattern in CYAN_PATTERNS:
if re.search(pattern, line):
instances.append((line_num, line, pattern))
break
except Exception as e:
print(f"Error reading {file_path}: {e}", file=sys.stderr)
return instances
def replace_decorative_cyan(file_path: Path, dry_run: bool = False) -> Tuple[int, int]:
"""Replace decorative cyan instances 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
# Get context (previous 2 and next 2 lines)
context_start = max(0, line_num - 3)
context_end = min(len(lines), line_num + 2)
context_lines = lines[context_start:context_end]
# Check if should preserve
if should_preserve(line, str(file_path), context_lines):
# Count preserved instances
for pattern in CYAN_PATTERNS:
if re.search(pattern, line, re.IGNORECASE):
preserved += 1
break
new_lines.append(line)
continue
# Replace decorative instances
line_replaced = False
for old, new in REPLACEMENTS.items():
if old in line:
# Double-check we shouldn't preserve
if not should_preserve(line, str(file_path), context_lines):
line = line.replace(old, new)
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='Replace decorative kodo-cyan with kodo-steel')
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_decorative_cyan(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()