diff --git a/.github/contrast-check.yml b/.github/contrast-check.yml new file mode 100644 index 00000000..c533c87b --- /dev/null +++ b/.github/contrast-check.yml @@ -0,0 +1,22 @@ +name: "Contrast Check" +on: + pull_request: + paths: + - 'css/brands.css' + +jobs: + contrast-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Fetch main branch + run: git fetch origin main:main + + - name: Run Contrast Check + run: | + chmod +x ./scripts/audit-contrast.sh + ./scripts/audit-contrast.sh \ No newline at end of file diff --git a/scripts/audit-contrast.sh b/scripts/audit-contrast.sh new file mode 100755 index 00000000..77ad2930 --- /dev/null +++ b/scripts/audit-contrast.sh @@ -0,0 +1,239 @@ +#!/bin/bash + +# WCAG Minimum Contrast Ratio +MIN_CONTRAST=4.5 + +FAILED=0 +ALL_RESOLVED=1 +NEEDS_MANUAL_REVIEW=0 + +# Function to calculate luminance +get_luminance() { + local color="$1" + + if [[ -z "$color" || "$color" == "#" ]]; then + echo 0 + return + fi + + color="${color#'#'}" + + if [[ ${#color} -ne 6 ]]; then + echo 0 + return + fi + + r=$(printf "%d" 0x${color:0:2} 2>/dev/null || echo 0) + g=$(printf "%d" 0x${color:2:2} 2>/dev/null || echo 0) + b=$(printf "%d" 0x${color:4:2} 2>/dev/null || echo 0) + + r=$(awk "BEGIN { print ($r/255 <= 0.03928) ? ($r/255)/12.92 : ((($r/255) + 0.055)/1.055) ^ 2.4 }") + g=$(awk "BEGIN { print ($g/255 <= 0.03928) ? ($g/255)/12.92 : ((($g/255) + 0.055)/1.055) ^ 2.4 }") + b=$(awk "BEGIN { print ($b/255 <= 0.03928) ? ($b/255)/12.92 : ((($b/255) + 0.055)/1.055) ^ 2.4 }") + + echo $(awk "BEGIN { print (0.2126 * $r) + (0.7152 * $g) + (0.0722 * $b) }") +} + +# Function to calculate contrast ratio +get_contrast_ratio() { + local lum1=$(get_luminance "$1") + local lum2=$(get_luminance "$2") + + if [[ -z "$lum1" || -z "$lum2" ]]; then + echo 0 + return + fi + + if (( $(awk "BEGIN { print ($lum1 > $lum2) ? 1 : 0 }") )); then + awk "BEGIN { printf \"%.5f\", ($lum1 + 0.05) / ($lum2 + 0.05) }" + else + awk "BEGIN { printf \"%.5f\", ($lum2 + 0.05) / ($lum1 + 0.05) }" + fi +} + +# Function to extract hex color from various CSS formats +extract_color() { + local input="$1" + local color="" + + # Try multiple extraction methods + if [[ "$input" =~ "#[0-9a-fA-F]{6}" ]]; then + # Direct hex color + color=$(echo "$input" | grep -o "#[0-9a-fA-F]\{6\}") + elif [[ "$input" =~ "1px solid #" ]]; then + # Border format + color=$(echo "$input" | sed -E 's/.*1px solid (#[0-9a-fA-F]{6}).*/\1/') + elif [[ "$input" =~ "solid #" ]]; then + # Alternative border format + color=$(echo "$input" | sed -E 's/.*solid (#[0-9a-fA-F]{6}).*/\1/') + elif [[ "$input" =~ "#" ]]; then + # Fallback for any hex + color=$(echo "$input" | grep -o "#[0-9a-fA-F]*" | head -1) + fi + + echo "$color" +} + +# Function to check contrast +check_contrast() { + local text_color="$1" + local background_color="$2" + local context="$3" + local border_color="$4" + local recommend_stroke="$5" + local is_background_check="$6" + local button_name="$7" + local check_failed=0 + + if [[ -z "$text_color" || -z "$background_color" ]]; then + return 0 + fi + + local contrast_ratio=$(get_contrast_ratio "$text_color" "$background_color") + + if [[ -z "$contrast_ratio" ]]; then + contrast_ratio=0 + fi + + contrast_ratio=$(printf "%.2f" "$contrast_ratio") + + if (( $(awk "BEGIN { print ($contrast_ratio < $MIN_CONTRAST) ? 1 : 0 }") )); then + # Check if we have the recommended border that compensates for the contrast issue + if [[ -n "$border_color" && "$border_color" == "$recommend_stroke" && "$is_background_check" -eq 1 ]]; then + echo "✅ [$context → $button_name] Contrast ratio $contrast_ratio fails WCAG but has a $recommend_stroke border → Treated as passing." + echo "✅ [$context → $button_name] Issue resolved by stroke → Fully passing." + check_failed=0 + else + echo "❌ [$context → $button_name] Contrast ratio $contrast_ratio fails WCAG — Recommend adding a $recommend_stroke stroke." + check_failed=1 + fi + else + echo "✅ [$context → $button_name] Contrast ratio $contrast_ratio passes WCAG" + check_failed=0 + fi + + return $check_failed +} + +# Function to process button CSS properties +process_button() { + local button_name="$1" + local text_color="$2" + local background_color="$3" + local border_color="$4" + local has_gradient="$5" + local button_failed=0 + + # Handle gradient case first + if [[ "$has_gradient" -eq 1 ]]; then + echo "🚩 [./css/brands.css → $button_name] Detected gradient background → Flagging for manual review." + NEEDS_MANUAL_REVIEW=1 + return + fi + + # Run all contrast checks for this button + if [[ -n "$text_color" && -n "$background_color" ]]; then + check_contrast "$text_color" "$background_color" "TEXT vs BUTTON" "$border_color" "" 0 "$button_name" + button_failed=$((button_failed | $?)) + fi + + if [[ -n "$background_color" ]]; then + check_contrast "#ffffff" "$background_color" "BUTTON vs LIGHT THEME" "$border_color" "#000000" 1 "$button_name" + button_failed=$((button_failed | $?)) + + check_contrast "#121212" "$background_color" "BUTTON vs DARK THEME" "$border_color" "#ffffff" 1 "$button_name" + button_failed=$((button_failed | $?)) + fi + + if [[ $button_failed -eq 1 ]]; then + FAILED=1 + ALL_RESOLVED=0 + fi +} + +# ---- FIXED GIT DIFF HANDLING ---- +new_css=$(git diff origin/main --unified=0 -- css/brands.css | awk '/^\+.*(--button-text|--button-background|--button-border|background-image|\/\*|\.button-)/') + +if [[ -z "$new_css" ]]; then + echo "✅ No new CSS changes to check." + exit 0 +fi + +echo "🔍 Auditing new CSS changes..." + +# Variables for collecting button properties +BUTTON_NAME="" +TEXT_COLOR="" +BACKGROUND_COLOR="" +BORDER_COLOR="" +HAS_GRADIENT=0 + +# Process each line of CSS +while IFS= read -r line; do + # Extract Button Name + if [[ "$line" =~ /\* ]]; then + # If we have a previous button to process, do it now + if [[ -n "$BUTTON_NAME" && (-n "$TEXT_COLOR" || -n "$BACKGROUND_COLOR") ]]; then + process_button "$BUTTON_NAME" "$TEXT_COLOR" "$BACKGROUND_COLOR" "$BORDER_COLOR" "$HAS_GRADIENT" + fi + + # Start a new button + BUTTON_NAME=$(echo "$line" | sed -E 's|\/\* *([^*]+) *\*\/|\1|' | xargs) + TEXT_COLOR="" + BACKGROUND_COLOR="" + BORDER_COLOR="" + HAS_GRADIENT=0 + fi + + if [[ "$line" =~ \.button- ]]; then + BUTTON_NAME=$(echo "$line" | sed -E 's|.*\.button-([^ ]+).*|\1|' | tr -d '{') + fi + + # Collect properties + if [[ "$line" =~ --button-text ]]; then + TEXT_COLOR=$(echo "$line" | awk -F '--button-text:' '{print $2}' | awk '{print $1}' | tr -d ';') + fi + + if [[ "$line" =~ --button-background ]]; then + BACKGROUND_COLOR=$(echo "$line" | awk -F '--button-background:' '{print $2}' | awk '{print $1}' | tr -d ';') + fi + + # Extract border color + if [[ "$line" =~ --button-border ]]; then + BORDER_COLOR=$(extract_color "$line") + fi + + # Check for gradient + if [[ "$line" =~ background-image ]]; then + HAS_GRADIENT=1 + fi + + # Process at end of button block + if [[ "$line" =~ '}' && -n "$BUTTON_NAME" ]]; then + process_button "$BUTTON_NAME" "$TEXT_COLOR" "$BACKGROUND_COLOR" "$BORDER_COLOR" "$HAS_GRADIENT" + + # Reset for next button + BUTTON_NAME="" + TEXT_COLOR="" + BACKGROUND_COLOR="" + BORDER_COLOR="" + HAS_GRADIENT=0 + fi +done <<< "$new_css" + +# Process any remaining button that wasn't ended with '}' +if [[ -n "$BUTTON_NAME" && (-n "$TEXT_COLOR" || -n "$BACKGROUND_COLOR") ]]; then + process_button "$BUTTON_NAME" "$TEXT_COLOR" "$BACKGROUND_COLOR" "$BORDER_COLOR" "$HAS_GRADIENT" +fi + +# ✅ Final report +if [[ "$NEEDS_MANUAL_REVIEW" -eq 1 ]]; then + echo "⚠️ Manual review required for gradients!" + exit 1 +elif [[ "$ALL_RESOLVED" -eq 1 ]]; then + echo "✅ All contrast checks passed!" + exit 0 +else + echo "❌ Contrast issues found!" + exit 1 +fi \ No newline at end of file