- 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)
265 lines
8.9 KiB
Python
Executable file
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()
|