- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
274 lines
9.1 KiB
PHP
274 lines
9.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Design;
|
|
|
|
use App\Framework\Design\Component\Component;
|
|
use App\Framework\Design\Component\ComponentCategory;
|
|
use App\Framework\Design\Component\ComponentPattern;
|
|
use App\Framework\Design\Component\ComponentRegistry;
|
|
use App\Framework\Design\Component\ComponentState;
|
|
use App\Framework\Filesystem\ValueObjects\FilePath;
|
|
|
|
/**
|
|
* Scans CSS files to detect and catalog UI components
|
|
*/
|
|
final readonly class ComponentScanner
|
|
{
|
|
/**
|
|
* @param array<string|FilePath> $cssFiles
|
|
*/
|
|
public function scanComponents(array $cssFiles): ComponentRegistry
|
|
{
|
|
$components = [];
|
|
|
|
foreach ($cssFiles as $cssFile) {
|
|
if (! $cssFile instanceof FilePath) {
|
|
$cssFile = new FilePath($cssFile);
|
|
}
|
|
|
|
if (! $cssFile->exists()) {
|
|
continue;
|
|
}
|
|
|
|
$cssContent = file_get_contents($cssFile->toString());
|
|
if ($cssContent === false) {
|
|
continue;
|
|
}
|
|
|
|
$fileComponents = $this->extractComponentsFromCss($cssContent, $cssFile->toString());
|
|
$components = array_merge($components, $fileComponents);
|
|
}
|
|
|
|
return new ComponentRegistry($components);
|
|
}
|
|
|
|
/**
|
|
* @return array<Component>
|
|
*/
|
|
private function extractComponentsFromCss(string $cssContent, string $filePath): array
|
|
{
|
|
$components = [];
|
|
$processedComponents = [];
|
|
|
|
// Remove comments
|
|
$cleanedContent = preg_replace('/\/\*.*?\*\//s', '', $cssContent);
|
|
$cssContent = $cleanedContent !== null ? $cleanedContent : $cssContent;
|
|
|
|
// Find all CSS selectors with improved regex that handles nested braces
|
|
preg_match_all('/([^{}]+)\s*{([^{}]*(?:{[^{}]*}[^{}]*)*)}/s', $cssContent, $matches, PREG_SET_ORDER);
|
|
|
|
foreach ($matches as $match) {
|
|
$selectors = $match[1];
|
|
$cssRules = trim($match[2]);
|
|
|
|
// Skip empty rules
|
|
if (empty($cssRules)) {
|
|
continue;
|
|
}
|
|
|
|
// Clean and split selectors
|
|
$selectorList = array_map('trim', explode(',', $selectors));
|
|
|
|
foreach ($selectorList as $selector) {
|
|
// Clean up the selector
|
|
$selector = trim($selector);
|
|
|
|
// Skip @rules, :root, and other non-component selectors
|
|
if (strpos($selector, '@') === 0 || $selector === ':root' || empty($selector)) {
|
|
continue;
|
|
}
|
|
|
|
$component = $this->analyzeSelector($selector, $cssRules, $filePath);
|
|
if ($component !== null) {
|
|
// Avoid duplicates based on component name
|
|
$key = $component->name . '_' . $component->state->value;
|
|
if (! isset($processedComponents[$key])) {
|
|
$components[] = $component;
|
|
$processedComponents[$key] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $components;
|
|
}
|
|
|
|
private function analyzeSelector(string $selector, string $cssRules, string $filePath): ?Component
|
|
{
|
|
// Clean up selector - remove :where, :is wrappers
|
|
$cleanedSelector = preg_replace('/:where\s*\((.*?)\)/', '$1', $selector);
|
|
$selector = $cleanedSelector !== null ? $cleanedSelector : $selector;
|
|
|
|
$cleanedSelector = preg_replace('/:is\s*\((.*?)\)/', '$1', $selector);
|
|
$selector = $cleanedSelector !== null ? $cleanedSelector : $selector;
|
|
|
|
// Ensure selector is non-empty string
|
|
if (empty($selector)) {
|
|
return null;
|
|
}
|
|
|
|
// Skip pseudo-elements and certain pseudo-classes
|
|
if (preg_match('/::/', $selector) || preg_match('/:not\(/', $selector)) {
|
|
return null;
|
|
}
|
|
|
|
// Skip complex selectors that are not component-like
|
|
if (preg_match('/^\s*(html|body|main|header|footer|section|article|aside|nav|h1|h2|h3|h4|h5|h6|p|div|span|a|ul|ol|li|img|br|hr)\s*$/i', $selector)) {
|
|
return null;
|
|
}
|
|
|
|
// Skip overly complex selectors (more than 3 parts)
|
|
if (substr_count($selector, ' ') > 3) {
|
|
return null;
|
|
}
|
|
|
|
// Skip selectors that are clearly not components
|
|
if (preg_match('/^\s*(\*|::before|::after|\[|>|\+|~)/i', $selector)) {
|
|
return null;
|
|
}
|
|
|
|
$componentName = null;
|
|
|
|
// Extract main class name - improved regex to handle more cases
|
|
if (preg_match('/\.([a-zA-Z][a-zA-Z0-9_-]*)(?:\s|:|$|>|\+|~|\[|,)/', $selector, $matches)) {
|
|
$componentName = $matches[1];
|
|
|
|
// Skip utility-only classes
|
|
if (strlen($componentName) <= 2) {
|
|
return null;
|
|
}
|
|
}
|
|
// Also handle element-based components (like button, input) - but be more selective
|
|
elseif (preg_match('/^(button|input|select|textarea|table|form|dialog)(?:\s|:|$|\[)/', $selector, $matches)) {
|
|
$componentName = $matches[1];
|
|
}
|
|
// Handle attribute selectors like input[type="button"]
|
|
elseif (preg_match('/(button|input|select|textarea)\[([^\]]+)\]/', $selector, $matches)) {
|
|
$elementType = $matches[1];
|
|
$attributes = $matches[2];
|
|
|
|
// Create a meaningful name based on element and attributes
|
|
if (preg_match('/type\s*=\s*["\']([^"\']+)["\']/', $attributes)) {
|
|
preg_match('/type\s*=\s*["\']([^"\']+)["\']/', $attributes, $typeMatches);
|
|
$componentName = $elementType . '-' . $typeMatches[1];
|
|
} else {
|
|
$componentName = $elementType;
|
|
}
|
|
}
|
|
|
|
if ($componentName !== null) {
|
|
// Categorize component
|
|
$category = $this->categorizeComponent($componentName);
|
|
$pattern = $this->detectPattern($componentName);
|
|
$state = $this->extractState($selector);
|
|
|
|
return new Component(
|
|
name: $componentName,
|
|
selector: $selector,
|
|
cssRules: $cssRules,
|
|
category: $category,
|
|
pattern: $pattern,
|
|
state: $state,
|
|
filePath: $filePath
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function categorizeComponent(string $className): ComponentCategory
|
|
{
|
|
$lowerName = strtolower($className);
|
|
|
|
// Button components
|
|
if (preg_match('/(btn|button)/i', $lowerName)) {
|
|
return ComponentCategory::BUTTON;
|
|
}
|
|
|
|
// Navigation components
|
|
if (preg_match('/(nav|menu|breadcrumb|tab|sidebar|aside)/i', $lowerName)) {
|
|
return ComponentCategory::NAVIGATION;
|
|
}
|
|
|
|
// Form components
|
|
if (preg_match('/(form|input|select|checkbox|radio|field|autosave|textarea)/i', $lowerName)) {
|
|
return ComponentCategory::FORM;
|
|
}
|
|
|
|
// Card components
|
|
if (preg_match('/(card|panel|box|tile)/i', $lowerName)) {
|
|
return ComponentCategory::CARD;
|
|
}
|
|
|
|
// Alert/notification components
|
|
if (preg_match('/(alert|notification|message|toast|status|error|warning|success)/i', $lowerName)) {
|
|
return ComponentCategory::FEEDBACK;
|
|
}
|
|
|
|
// Layout components
|
|
if (preg_match('/(container|grid|row|col|layout|wrapper|section|main|article)/i', $lowerName)) {
|
|
return ComponentCategory::LAYOUT;
|
|
}
|
|
|
|
// Typography components
|
|
if (preg_match('/(heading|title|text|font|headline|paragraph)/i', $lowerName)) {
|
|
return ComponentCategory::TYPOGRAPHY;
|
|
}
|
|
|
|
// Additional specific components
|
|
if (preg_match('/(header|footer|lightbox|modal|dialog)/i', $lowerName)) {
|
|
return ComponentCategory::LAYOUT;
|
|
}
|
|
|
|
if (preg_match('/(csrf)/i', $lowerName)) {
|
|
return ComponentCategory::FORM;
|
|
}
|
|
|
|
// Element-based components
|
|
if (preg_match('/^(table)$/i', $lowerName)) {
|
|
return ComponentCategory::LAYOUT;
|
|
}
|
|
|
|
return ComponentCategory::OTHER;
|
|
}
|
|
|
|
private function detectPattern(string $className): ComponentPattern
|
|
{
|
|
// BEM pattern (block__element--modifier)
|
|
if (preg_match('/^[a-z][a-z0-9]*(__[a-z][a-z0-9]*)?(-{2}[a-z][a-z0-9]*)?$/', $className)) {
|
|
return ComponentPattern::BEM;
|
|
}
|
|
|
|
// Utility pattern (single purpose, often prefixed)
|
|
if (preg_match('/^(m|p|text|bg|border|flex|grid|w|h)-/', $className)) {
|
|
return ComponentPattern::UTILITY;
|
|
}
|
|
|
|
// Traditional component pattern
|
|
return ComponentPattern::TRADITIONAL;
|
|
}
|
|
|
|
private function extractState(string $selector): ComponentState
|
|
{
|
|
if (strpos($selector, ':hover') !== false) {
|
|
return ComponentState::HOVER;
|
|
}
|
|
|
|
if (strpos($selector, ':focus') !== false) {
|
|
return ComponentState::FOCUS;
|
|
}
|
|
|
|
if (strpos($selector, ':active') !== false) {
|
|
return ComponentState::ACTIVE;
|
|
}
|
|
|
|
if (strpos($selector, ':disabled') !== false) {
|
|
return ComponentState::DISABLED;
|
|
}
|
|
|
|
return ComponentState::DEFAULT;
|
|
}
|
|
}
|