diff options
| author | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-01-16 08:30:14 +0900 |
|---|---|---|
| committer | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-01-16 08:30:14 +0900 |
| commit | 3fbb9a18372f2b6a675dd6c039ba52be76f3eeb4 (patch) | |
| tree | aa694a36cdd323a7853672ee7a2ba60409ac3b06 /ui/shadcn/.claude/hooks | |
updates
Diffstat (limited to 'ui/shadcn/.claude/hooks')
| -rwxr-xr-x | ui/shadcn/.claude/hooks/check-accessibility.sh | 197 | ||||
| -rwxr-xr-x | ui/shadcn/.claude/hooks/format-tailwind.sh | 76 | ||||
| -rwxr-xr-x | ui/shadcn/.claude/hooks/optimize-imports.sh | 121 | ||||
| -rwxr-xr-x | ui/shadcn/.claude/hooks/validate-components.sh | 131 |
4 files changed, 525 insertions, 0 deletions
diff --git a/ui/shadcn/.claude/hooks/check-accessibility.sh b/ui/shadcn/.claude/hooks/check-accessibility.sh new file mode 100755 index 0000000..24be077 --- /dev/null +++ b/ui/shadcn/.claude/hooks/check-accessibility.sh @@ -0,0 +1,197 @@ +#!/bin/bash + +# Check accessibility compliance after component modifications +# This hook runs after Write/Edit/MultiEdit operations + +# Colors for output +RED='\033[0;31m' +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Read tool result from stdin +TOOL_RESULT=$(cat) +TOOL_NAME=$(echo "$TOOL_RESULT" | jq -r '.tool_name // empty' 2>/dev/null) + +# Only process if it's a file modification tool +if [[ "$TOOL_NAME" != "Write" ]] && [[ "$TOOL_NAME" != "Edit" ]] && [[ "$TOOL_NAME" != "MultiEdit" ]]; then + echo "$TOOL_RESULT" + exit 0 +fi + +# Extract file path +FILE_PATH=$(echo "$TOOL_RESULT" | jq -r '.tool_input.file_path // empty' 2>/dev/null) + +# Only process component files +if [[ ! "$FILE_PATH" =~ \.(tsx?|jsx?)$ ]] || [[ ! "$FILE_PATH" =~ component ]]; then + echo "$TOOL_RESULT" + exit 0 +fi + +# Check if file exists +if [ ! -f "$FILE_PATH" ]; then + echo "$TOOL_RESULT" + exit 0 +fi + +echo -e "${BLUE}🔍 Checking accessibility in $FILE_PATH...${NC}" >&2 + +# Initialize counters +ISSUES=0 +WARNINGS=0 + +# Function to check patterns +check_pattern() { + local pattern="$1" + local message="$2" + local type="$3" # "error" or "warning" + + if grep -q "$pattern" "$FILE_PATH"; then + if [ "$type" = "error" ]; then + echo -e "${RED}❌ A11y Issue: $message${NC}" >&2 + ((ISSUES++)) + else + echo -e "${YELLOW}⚠️ A11y Warning: $message${NC}" >&2 + ((WARNINGS++)) + fi + return 1 + fi + return 0 +} + +# Function to check for missing patterns +check_missing() { + local pattern="$1" + local context="$2" + local message="$3" + + if grep -q "$context" "$FILE_PATH"; then + if ! grep -q "$pattern" "$FILE_PATH"; then + echo -e "${YELLOW}⚠️ A11y Warning: $message${NC}" >&2 + ((WARNINGS++)) + return 1 + fi + fi + return 0 +} + +# Check for interactive elements without keyboard support +if grep -qE '<(button|a|input|select|textarea)' "$FILE_PATH"; then + # Check for onClick without onKeyDown/onKeyPress + if grep -q 'onClick=' "$FILE_PATH"; then + if ! grep -qE '(onKeyDown|onKeyPress|onKeyUp)=' "$FILE_PATH"; then + echo -e "${YELLOW}⚠️ A11y Warning: onClick handlers should have keyboard alternatives${NC}" >&2 + ((WARNINGS++)) + fi + fi + + # Check for proper button usage + if grep -q '<div.*onClick=' "$FILE_PATH"; then + echo -e "${YELLOW}⚠️ A11y Warning: Use <button> instead of <div> with onClick for interactive elements${NC}" >&2 + ((WARNINGS++)) + fi +fi + +# Check for images without alt text +if grep -qE '<img[^>]*>' "$FILE_PATH"; then + IMG_TAGS=$(grep -o '<img[^>]*>' "$FILE_PATH") + while IFS= read -r img; do + if ! echo "$img" | grep -q 'alt='; then + echo -e "${RED}❌ A11y Issue: Image missing alt attribute${NC}" >&2 + ((ISSUES++)) + fi + done <<< "$IMG_TAGS" +fi + +# Check for form elements +if grep -qE '<(input|select|textarea)' "$FILE_PATH"; then + # Check for labels + check_missing "label" "input\|select\|textarea" "Form elements should have associated labels" + + # Check for aria-required on required fields + if grep -q 'required' "$FILE_PATH"; then + check_missing "aria-required" "required" "Required fields should have aria-required attribute" + fi + + # Check for error messages + if grep -q 'error' "$FILE_PATH"; then + check_missing "aria-describedby\|aria-errormessage" "error" "Error messages should be associated with form fields" + fi +fi + +# Check for ARIA attributes +if grep -q '<button' "$FILE_PATH"; then + # Icon-only buttons should have aria-label + if grep -qE '<button[^>]*>[\s]*<(svg|Icon)' "$FILE_PATH"; then + check_missing "aria-label" "<button.*Icon\|<button.*svg" "Icon-only buttons need aria-label" + fi +fi + +# Check for modals/dialogs +if grep -qE '(Dialog|Modal|Sheet|Popover)' "$FILE_PATH"; then + check_missing "aria-labelledby\|aria-label" "Dialog\|Modal" "Dialogs should have aria-labelledby or aria-label" + check_missing "aria-describedby" "DialogDescription" "Dialogs should have aria-describedby for descriptions" +fi + +# Check for proper heading hierarchy +if grep -qE '<h[1-6]' "$FILE_PATH"; then + # Extract all heading levels + HEADINGS=$(grep -o '<h[1-6]' "$FILE_PATH" | sed 's/<h//' | sort -n) + PREV=0 + for h in $HEADINGS; do + if [ $PREV -ne 0 ] && [ $((h - PREV)) -gt 1 ]; then + echo -e "${YELLOW}⚠️ A11y Warning: Heading hierarchy skip detected (h$PREV to h$h)${NC}" >&2 + ((WARNINGS++)) + break + fi + PREV=$h + done +fi + +# Check for color contrast (basic check for hardcoded colors) +if grep -qE '(text-(white|black)|bg-(white|black))' "$FILE_PATH"; then + if grep -q 'text-white.*bg-white\|text-black.*bg-black' "$FILE_PATH"; then + echo -e "${RED}❌ A11y Issue: Potential color contrast issue detected${NC}" >&2 + ((ISSUES++)) + fi +fi + +# Check for focus management +if grep -qE '(focus:outline-none|outline-none)' "$FILE_PATH"; then + if ! grep -q 'focus-visible:\|focus:ring\|focus:border' "$FILE_PATH"; then + echo -e "${RED}❌ A11y Issue: Removing outline without providing alternative focus indicator${NC}" >&2 + ((ISSUES++)) + fi +fi + +# Check for live regions +if grep -q 'toast\|notification\|alert\|message' "$FILE_PATH"; then + check_missing "aria-live\|role=\"alert\"\|role=\"status\"" "toast\|notification\|alert" "Dynamic content should use live regions" +fi + +# Check for lists +if grep -qE '<li[^>]*>' "$FILE_PATH"; then + if ! grep -qE '<(ul|ol)[^>]*>' "$FILE_PATH"; then + echo -e "${YELLOW}⚠️ A11y Warning: <li> elements should be wrapped in <ul> or <ol>${NC}" >&2 + ((WARNINGS++)) + fi +fi + +# Summary +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2 +if [ $ISSUES -eq 0 ] && [ $WARNINGS -eq 0 ]; then + echo -e "${GREEN}✅ Accessibility check passed!${NC}" >&2 +else + if [ $ISSUES -gt 0 ]; then + echo -e "${RED}Found $ISSUES accessibility issues${NC}" >&2 + fi + if [ $WARNINGS -gt 0 ]; then + echo -e "${YELLOW}Found $WARNINGS accessibility warnings${NC}" >&2 + fi + echo -e "${BLUE}Consider running a full accessibility audit with axe-core${NC}" >&2 +fi + +# Pass through the original result +echo "$TOOL_RESULT" +exit 0
\ No newline at end of file diff --git a/ui/shadcn/.claude/hooks/format-tailwind.sh b/ui/shadcn/.claude/hooks/format-tailwind.sh new file mode 100755 index 0000000..67666b9 --- /dev/null +++ b/ui/shadcn/.claude/hooks/format-tailwind.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# Format and sort Tailwind classes after file modifications +# This hook runs after Write/Edit/MultiEdit operations + +# Read tool result from stdin +TOOL_RESULT=$(cat) +TOOL_NAME=$(echo "$TOOL_RESULT" | jq -r '.tool_name // empty' 2>/dev/null) + +# Only process if it's a file modification tool +if [[ "$TOOL_NAME" != "Write" ]] && [[ "$TOOL_NAME" != "Edit" ]] && [[ "$TOOL_NAME" != "MultiEdit" ]]; then + echo "$TOOL_RESULT" + exit 0 +fi + +# Extract file path +FILE_PATH=$(echo "$TOOL_RESULT" | jq -r '.tool_input.file_path // empty' 2>/dev/null) + +# Only process TypeScript/JavaScript files +if [[ ! "$FILE_PATH" =~ \.(tsx?|jsx?)$ ]]; then + echo "$TOOL_RESULT" + exit 0 +fi + +# Check if file exists and we can process it +if [ -f "$FILE_PATH" ]; then + # Check for prettier and format if available + if command -v npx &> /dev/null && [ -f "package.json" ]; then + # Check if prettier is installed + if npm list prettier &>/dev/null || npm list -g prettier &>/dev/null; then + echo "🎨 Formatting $FILE_PATH with Prettier..." >&2 + npx prettier --write "$FILE_PATH" 2>/dev/null + fi + + # Check if prettier-plugin-tailwindcss is available for class sorting + if npm list prettier-plugin-tailwindcss &>/dev/null; then + echo "🎨 Sorting Tailwind classes in $FILE_PATH..." >&2 + npx prettier --write "$FILE_PATH" --plugin=prettier-plugin-tailwindcss 2>/dev/null + fi + fi + + # Additional validation for shadcn components + if [[ "$FILE_PATH" =~ components/ui/ ]] || [[ "$FILE_PATH" =~ src/components/ui/ ]]; then + # Count Tailwind classes (rough estimate) + CLASS_COUNT=$(grep -o 'className=' "$FILE_PATH" | wc -l) + CN_COUNT=$(grep -o 'cn(' "$FILE_PATH" | wc -l) + + if [ $CLASS_COUNT -gt 0 ] && [ $CN_COUNT -eq 0 ]; then + echo "💡 Tip: Consider using the cn() utility for className merging in $FILE_PATH" >&2 + fi + + # Check for common Tailwind mistakes + if grep -q 'className="[^"]* [^"]*"' "$FILE_PATH"; then + echo "⚠️ Warning: Double spaces detected in className attributes" >&2 + fi + + # Check for responsive modifiers in correct order + if grep -qE 'className="[^"]*(lg:|xl:|2xl:)[^"]*(sm:|md:)' "$FILE_PATH"; then + echo "⚠️ Warning: Responsive modifiers should be in mobile-first order (sm → md → lg → xl)" >&2 + fi + + # Check for dark mode classes + if grep -q 'dark:' "$FILE_PATH"; then + echo "✓ Dark mode classes detected - ensure CSS variables are used for consistency" >&2 + fi + + # Count CVA usage + if grep -q 'cva(' "$FILE_PATH"; then + echo "✓ CVA variants detected - good for component flexibility" >&2 + fi + fi +fi + +# Pass through the original result +echo "$TOOL_RESULT" +exit 0
\ No newline at end of file diff --git a/ui/shadcn/.claude/hooks/optimize-imports.sh b/ui/shadcn/.claude/hooks/optimize-imports.sh new file mode 100755 index 0000000..1b4f206 --- /dev/null +++ b/ui/shadcn/.claude/hooks/optimize-imports.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +# Optimize and clean up imports when session ends +# This hook runs on Stop event + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}🔧 Running import optimization...${NC}" >&2 + +# Find all TypeScript/JavaScript files in components directory +COMPONENT_FILES=$(find . -path "*/components/*.tsx" -o -path "*/components/*.ts" -o -path "*/components/*.jsx" -o -path "*/components/*.js" 2>/dev/null) + +if [ -z "$COMPONENT_FILES" ]; then + echo -e "${YELLOW}No component files found to optimize${NC}" >&2 + exit 0 +fi + +# Count total files +TOTAL_FILES=$(echo "$COMPONENT_FILES" | wc -l) +OPTIMIZED=0 + +echo -e "${BLUE}Checking $TOTAL_FILES component files...${NC}" >&2 + +# Process each file +while IFS= read -r FILE; do + if [ ! -f "$FILE" ]; then + continue + fi + + CHANGES_MADE=false + + # Check for unused imports (basic check) + # This is a simple heuristic - a proper tool like ESLint would be better + IMPORTS=$(grep -E "^import .* from" "$FILE" 2>/dev/null) + + while IFS= read -r IMPORT_LINE; do + # Extract imported names (basic regex, doesn't handle all cases) + if [[ "$IMPORT_LINE" =~ import[[:space:]]+\{([^}]+)\} ]]; then + NAMES="${BASH_REMATCH[1]}" + # Check each imported name + IFS=',' read -ra NAME_ARRAY <<< "$NAMES" + for NAME in "${NAME_ARRAY[@]}"; do + # Clean up the name (remove spaces and 'as' aliases) + CLEAN_NAME=$(echo "$NAME" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | cut -d' ' -f1) + # Check if the name is used in the file (excluding the import line) + if ! grep -q "$CLEAN_NAME" "$FILE" | grep -v "^import"; then + echo -e "${YELLOW} ⚠️ Potentially unused import '$CLEAN_NAME' in $FILE${NC}" >&2 + fi + done + fi + done <<< "$IMPORTS" + + # Check import order (should be external -> internal -> relative) + IMPORT_BLOCK=$(awk '/^import/,/^[^i]/' "$FILE" | grep "^import" 2>/dev/null) + + # Categories + REACT_IMPORTS="" + EXTERNAL_IMPORTS="" + INTERNAL_IMPORTS="" + RELATIVE_IMPORTS="" + UI_IMPORTS="" + + while IFS= read -r LINE; do + if [[ "$LINE" =~ from[[:space:]]+[\'\"]react ]]; then + REACT_IMPORTS="$REACT_IMPORTS$LINE\n" + elif [[ "$LINE" =~ from[[:space:]]+[\'\"]@/components/ui ]]; then + UI_IMPORTS="$UI_IMPORTS$LINE\n" + elif [[ "$LINE" =~ from[[:space:]]+[\'\"]@/ ]]; then + INTERNAL_IMPORTS="$INTERNAL_IMPORTS$LINE\n" + elif [[ "$LINE" =~ from[[:space:]]+[\'\"]\.\.?/ ]]; then + RELATIVE_IMPORTS="$RELATIVE_IMPORTS$LINE\n" + else + EXTERNAL_IMPORTS="$EXTERNAL_IMPORTS$LINE\n" + fi + done <<< "$IMPORT_BLOCK" + + # Check for duplicate imports from same module + MODULES=$(echo "$IMPORT_BLOCK" | grep -oE "from ['\"][^'\"]+['\"]" | sort | uniq -d) + if [ -n "$MODULES" ]; then + echo -e "${YELLOW} ⚠️ Duplicate imports detected in $FILE${NC}" >&2 + echo "$MODULES" | while read -r MODULE; do + echo -e "${YELLOW} $MODULE${NC}" >&2 + done + fi + + # Check for specific shadcn/ui optimizations + if [[ "$FILE" =~ components/ui/ ]]; then + # Check if using barrel imports when individual imports would be better + if grep -q "from '@/components/ui'" "$FILE"; then + echo -e "${YELLOW} 💡 Tip: Import UI components directly (e.g., from '@/components/ui/button')${NC}" >&2 + fi + + # Check for missing cn utility import when className is used + if grep -q "className=" "$FILE" && ! grep -q "import.*cn.*from" "$FILE"; then + if grep -q "clsx\|classnames" "$FILE"; then + echo -e "${YELLOW} 💡 Consider using cn() utility from '@/lib/utils' instead of clsx/classnames${NC}" >&2 + fi + fi + fi + + ((OPTIMIZED++)) +done <<< "$COMPONENT_FILES" + +# Summary +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2 +echo -e "${GREEN}✅ Import optimization check complete!${NC}" >&2 +echo -e "${BLUE} Files checked: $OPTIMIZED/$TOTAL_FILES${NC}" >&2 + +# Additional recommendations +if command -v npx &> /dev/null && [ -f "package.json" ]; then + echo -e "${BLUE}💡 For automatic import optimization, consider:${NC}" >&2 + echo -e "${BLUE} • ESLint with eslint-plugin-import${NC}" >&2 + echo -e "${BLUE} • prettier-plugin-organize-imports${NC}" >&2 + echo -e "${BLUE} • TypeScript's organizeImports feature${NC}" >&2 +fi + +exit 0
\ No newline at end of file diff --git a/ui/shadcn/.claude/hooks/validate-components.sh b/ui/shadcn/.claude/hooks/validate-components.sh new file mode 100755 index 0000000..190bcf6 --- /dev/null +++ b/ui/shadcn/.claude/hooks/validate-components.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +# Validate shadcn/ui component structure before changes +# This hook runs before Write/Edit/MultiEdit operations + +# Colors for output +RED='\033[0;31m' +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# Read tool input from stdin +TOOL_INPUT=$(cat) +TOOL_NAME=$(echo "$TOOL_INPUT" | jq -r '.tool_name // empty') +FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.tool_input.file_path // empty') + +# Only validate component files +if [[ ! "$FILE_PATH" =~ components/ui/.*\.tsx$ ]] && [[ ! "$FILE_PATH" =~ src/components/ui/.*\.tsx$ ]]; then + echo "$TOOL_INPUT" + exit 0 +fi + +# Extract component name from file path +COMPONENT_NAME=$(basename "$FILE_PATH" .tsx) + +# Validation flags +HAS_ERRORS=0 +WARNINGS="" + +# Function to log warnings +log_warning() { + WARNINGS="${WARNINGS}⚠️ $1\n" +} + +# Function to log errors +log_error() { + echo -e "${RED}❌ Component Validation Error: $1${NC}" >&2 + HAS_ERRORS=1 +} + +# Check if this is a Write operation for a new file +if [ "$TOOL_NAME" = "Write" ] && [ ! -f "$FILE_PATH" ]; then + # New component file - check for required patterns in content + CONTENT=$(echo "$TOOL_INPUT" | jq -r '.tool_input.content // empty') + + # Check for forwardRef pattern + if [[ ! "$CONTENT" =~ "React.forwardRef" ]] && [[ ! "$CONTENT" =~ "forwardRef" ]]; then + log_warning "New component should use React.forwardRef for ref forwarding" + fi + + # Check for displayName + if [[ ! "$CONTENT" =~ "displayName" ]]; then + log_warning "Component should have displayName for debugging" + fi + + # Check for TypeScript types + if [[ ! "$CONTENT" =~ "interface.*Props" ]] && [[ ! "$CONTENT" =~ "type.*Props" ]]; then + log_warning "Component should have TypeScript prop types defined" + fi + + # Check for cn utility usage + if [[ "$CONTENT" =~ "className" ]] && [[ ! "$CONTENT" =~ "cn(" ]]; then + log_warning "Consider using cn() utility for className merging" + fi + + # Check for accessibility attributes in interactive components + if [[ "$CONTENT" =~ "<button" ]] || [[ "$CONTENT" =~ "<a " ]] || [[ "$CONTENT" =~ "<input" ]]; then + if [[ ! "$CONTENT" =~ "aria-" ]] && [[ ! "$CONTENT" =~ "role=" ]]; then + log_warning "Interactive components should include ARIA attributes for accessibility" + fi + fi +fi + +# Check for Edit operations on existing files +if [ "$TOOL_NAME" = "Edit" ] || [ "$TOOL_NAME" = "MultiEdit" ]; then + # Check if removing important patterns + OLD_STRING=$(echo "$TOOL_INPUT" | jq -r '.tool_input.old_string // empty') + NEW_STRING=$(echo "$TOOL_INPUT" | jq -r '.tool_input.new_string // empty') + + # Check if removing forwardRef + if [[ "$OLD_STRING" =~ "forwardRef" ]] && [[ ! "$NEW_STRING" =~ "forwardRef" ]]; then + log_warning "Removing forwardRef might break ref forwarding" + fi + + # Check if removing displayName + if [[ "$OLD_STRING" =~ "displayName" ]] && [[ ! "$NEW_STRING" =~ "displayName" ]]; then + log_warning "Removing displayName will make debugging harder" + fi + + # Check if removing TypeScript types + if [[ "$OLD_STRING" =~ ": React.FC" ]] || [[ "$OLD_STRING" =~ ": FC" ]]; then + if [[ ! "$NEW_STRING" =~ ": React.FC" ]] && [[ ! "$NEW_STRING" =~ ": FC" ]]; then + log_warning "Consider maintaining TypeScript types for components" + fi + fi +fi + +# Special validation for specific component types +case "$COMPONENT_NAME" in + button|input|select|textarea) + if [ "$TOOL_NAME" = "Write" ]; then + CONTENT=$(echo "$TOOL_INPUT" | jq -r '.tool_input.content // empty') + if [[ ! "$CONTENT" =~ "disabled" ]]; then + log_warning "Form components should handle disabled state" + fi + fi + ;; + dialog|modal|sheet|alert-dialog) + if [ "$TOOL_NAME" = "Write" ]; then + CONTENT=$(echo "$TOOL_INPUT" | jq -r '.tool_input.content // empty') + if [[ ! "$CONTENT" =~ "onOpenChange" ]] && [[ ! "$CONTENT" =~ "open" ]]; then + log_warning "Overlay components should have open/onOpenChange props" + fi + fi + ;; +esac + +# If there are errors, block the operation +if [ $HAS_ERRORS -eq 1 ]; then + exit 2 +fi + +# If there are warnings, show them but allow operation +if [ -n "$WARNINGS" ]; then + echo -e "${YELLOW}Component Validation Warnings:${NC}" >&2 + echo -e "$WARNINGS" >&2 +fi + +# Pass through the original input +echo "$TOOL_INPUT" +exit 0
\ No newline at end of file |
