add_violation( $file_path, $line_num, "Position/size changes on hover for non-clickable elements are PROHIBITED", $selector, "Professional business applications must remain static. Remove position/size changes from hover states. Color, background, opacity, and other visual changes are allowed.", 'critical' ); continue; // Skip further checks for this context } } // Get base properties for comparison $base_selector = ScssContextParser::get_base_selector($selector); $base_props = $base_properties[$base_selector] ?? []; foreach ($properties as $property => $value) { // Check if this is a position/size property (prohibited) if (ScssContextParser::is_position_property($property)) { // Check if it's redundant (same value as base) if (isset($base_props[$property]) && $base_props[$property] === $value) { $this->add_violation( $file_path, $line_num, "Redundant property in hover state - same value as base style", "$property: $value", "Remove unnecessary property redeclaration. This property already has this value.", 'low' ); } else { $this->add_violation( $file_path, $line_num, "CRITICAL: Position/size changes on hover/focus/active are PROHIBITED", "$property: $value", "Remove ALL position/size changes. Use ONLY: color, background-color, opacity, border-color, text-decoration, box-shadow, outline, filter", 'critical' ); } } // Check for transform property (always prohibited in hover states) if (str_starts_with($property, 'transform')) { $this->add_violation( $file_path, $line_num, "Transform effects in hover states are PROHIBITED", "$property: $value", "Remove transform from hover state. Elements must not move or rotate on interaction.", 'critical' ); } } } // Check for animation properties in any context if (isset($properties['animation']) || isset($properties['animation-name'])) { // Only rotation animations for spinners are allowed $animation_value = $properties['animation'] ?? $properties['animation-name'] ?? ''; if (!str_contains(strtolower($animation_value), 'spin') && !str_contains(strtolower($animation_value), 'rotate')) { $this->add_violation( $file_path, $line_num, "Animation property detected - only rotation for spinners allowed", "animation: $animation_value", "Only rotation animations for loading spinners are permitted. Remove all other animations.", 'critical' ); } } // Check for transition: all (always prohibited) if (isset($properties['transition']) && str_contains($properties['transition'], 'all')) { $this->add_violation( $file_path, $line_num, "'transition: all' is PROHIBITED", "transition: {$properties['transition']}", "Specify exact properties: 'transition: opacity 0.3s, background-color 0.3s'. Never use 'all'.", 'critical' ); } // Check for transitions on position/size properties if (isset($properties['transition'])) { $transition_value = $properties['transition']; $prohibited = ['transform', 'top', 'left', 'right', 'bottom', 'margin', 'padding', 'width', 'height']; foreach ($prohibited as $prop) { if (str_contains($transition_value, $prop)) { $this->add_violation( $file_path, $line_num, "Transitions on position/size are PROHIBITED", "transition: $transition_value", "Only allowed transitions: color, opacity, background-color, border-color. Remove position/size transitions.", 'critical' ); break; } } } // Check for will-change property if (isset($properties['will-change']) && $properties['will-change'] !== 'auto') { $this->add_violation( $file_path, $line_num, "will-change is PROHIBITED", "will-change: {$properties['will-change']}", "Remove will-change property. Elements must be static and predictable.", 'critical' ); } } // Additional line-by-line checks for keyframes and other patterns // that the context parser doesn't handle $this->check_keyframes($file_path, $lines); } /** * Check for @keyframes animations * Only pure rotation for spinners is allowed */ private function check_keyframes(string $file_path, array $lines): void { $in_keyframe = false; $keyframe_start_line = 0; $keyframe_content = ''; $brace_depth = 0; foreach ($lines as $line_num => $line) { $trimmed = trim($line); // Skip comments if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '/*')) { continue; } // Check for @keyframes start if (preg_match('/@keyframes\s+(\w+)/i', $line, $matches)) { $in_keyframe = true; $keyframe_start_line = $line_num + 1; $keyframe_content = ''; $brace_depth = 0; } if ($in_keyframe) { $keyframe_content .= $line . "\n"; $brace_depth += substr_count($line, '{'); $brace_depth -= substr_count($line, '}'); // End of keyframe block if ($brace_depth === 0 && str_contains($line, '}')) { // Check if keyframe contains prohibited animations if (preg_match('/transform\s*:\s*translate/i', $keyframe_content) || preg_match('/transform\s*:\s*scale/i', $keyframe_content) || preg_match('/(top|left|right|bottom|margin|padding|width|height)\s*:/i', $keyframe_content)) { $this->add_violation( $file_path, $keyframe_start_line, "Keyframe animations with movement/scaling are PROHIBITED", trim($lines[$keyframe_start_line - 1]), "Only rotation keyframes for spinners are allowed. Remove all translate/scale/position animations.", 'critical' ); } $in_keyframe = false; } } } } }