Initial commit
This commit is contained in:
467
skills/frontend-designer/scripts/audit_accessibility.sh
Executable file
467
skills/frontend-designer/scripts/audit_accessibility.sh
Executable file
@@ -0,0 +1,467 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Frontend Designer - Accessibility Audit
|
||||
# Comprehensive WCAG 2.1 AA compliance checker
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Counters
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
WARNING_COUNT=0
|
||||
|
||||
# Helper functions
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ PASS${NC} $1"
|
||||
((PASS_COUNT++))
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ FAIL${NC} $1"
|
||||
((FAIL_COUNT++))
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ WARN${NC} $1"
|
||||
((WARNING_COUNT++))
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ INFO${NC} $1"
|
||||
}
|
||||
|
||||
print_section() {
|
||||
echo ""
|
||||
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${MAGENTA}$1${NC}"
|
||||
echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
}
|
||||
|
||||
# Banner
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ Frontend Designer - Accessibility Audit ║"
|
||||
echo "║ WCAG 2.1 AA Compliance ║"
|
||||
echo "║ ║"
|
||||
echo "╚════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Get target
|
||||
if [ -z "$1" ]; then
|
||||
print_info "Usage: $0 <file.html|directory>"
|
||||
print_info "Example: $0 index.html"
|
||||
print_info "Example: $0 src/components/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET="$1"
|
||||
|
||||
# Check if target exists
|
||||
if [ ! -e "$TARGET" ]; then
|
||||
print_error "Target not found: $TARGET"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Section 1: HTML Structure
|
||||
print_section "1. HTML STRUCTURE & SEMANTICS"
|
||||
|
||||
check_html_lang() {
|
||||
if grep -q '<html[^>]*\slang=' "$1"; then
|
||||
print_success "HTML lang attribute present"
|
||||
else
|
||||
print_error "Missing lang attribute on <html>"
|
||||
echo " Fix: <html lang=\"en\">"
|
||||
fi
|
||||
}
|
||||
|
||||
check_page_title() {
|
||||
if grep -q '<title>' "$1"; then
|
||||
print_success "Page title present"
|
||||
else
|
||||
print_error "Missing <title> element"
|
||||
echo " Fix: Add <title>Page Title</title>"
|
||||
fi
|
||||
}
|
||||
|
||||
check_main_landmark() {
|
||||
if grep -q '<main' "$1" || grep -q 'role="main"' "$1"; then
|
||||
print_success "Main landmark present"
|
||||
else
|
||||
print_warning "No <main> landmark found"
|
||||
echo " Tip: Use <main> for primary content"
|
||||
fi
|
||||
}
|
||||
|
||||
check_heading_structure() {
|
||||
if grep -q '<h1' "$1"; then
|
||||
print_success "H1 heading found"
|
||||
|
||||
# Check for heading hierarchy
|
||||
local h1_count=$(grep -o '<h1' "$1" | wc -l)
|
||||
if [ "$h1_count" -eq 1 ]; then
|
||||
print_success "Single H1 (recommended)"
|
||||
else
|
||||
print_warning "Multiple H1 headings found ($h1_count)"
|
||||
echo " Tip: Use single H1 per page"
|
||||
fi
|
||||
else
|
||||
print_error "No H1 heading found"
|
||||
echo " Fix: Add <h1> for main page heading"
|
||||
fi
|
||||
}
|
||||
|
||||
check_semantic_html() {
|
||||
local semantic_tags=("nav" "header" "footer" "article" "section" "aside")
|
||||
local found=false
|
||||
|
||||
for tag in "${semantic_tags[@]}"; do
|
||||
if grep -q "<$tag" "$1"; then
|
||||
found=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$found" = true ]; then
|
||||
print_success "Semantic HTML elements used"
|
||||
else
|
||||
print_warning "Consider using semantic HTML (nav, header, footer, etc.)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Section 2: Images & Media
|
||||
print_section "2. IMAGES & MEDIA"
|
||||
|
||||
check_img_alt() {
|
||||
local img_count=$(grep -o '<img' "$1" | wc -l)
|
||||
|
||||
if [ "$img_count" -eq 0 ]; then
|
||||
print_info "No images found"
|
||||
else
|
||||
local alt_count=$(grep '<img' "$1" | grep -c 'alt=')
|
||||
|
||||
if [ "$alt_count" -eq "$img_count" ]; then
|
||||
print_success "All images have alt attributes ($img_count/$img_count)"
|
||||
else
|
||||
print_error "Images missing alt attributes ($alt_count/$img_count)"
|
||||
echo " Fix: Add alt=\"description\" to all images"
|
||||
echo " For decorative images use alt=\"\""
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_video_captions() {
|
||||
if grep -q '<video' "$1"; then
|
||||
if grep -q '<track' "$1"; then
|
||||
print_success "Video has captions/subtitles"
|
||||
else
|
||||
print_error "Video missing captions"
|
||||
echo " Fix: Add <track kind=\"captions\" src=\"captions.vtt\">"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Section 3: Forms
|
||||
print_section "3. FORMS & INPUTS"
|
||||
|
||||
check_form_labels() {
|
||||
local input_count=$(grep -o '<input' "$1" | wc -l)
|
||||
|
||||
if [ "$input_count" -eq 0 ]; then
|
||||
print_info "No form inputs found"
|
||||
else
|
||||
# Check for labels
|
||||
local label_count=$(grep -o '<label' "$1" | wc -l)
|
||||
local aria_label_count=$(grep '<input' "$1" | grep -c 'aria-label')
|
||||
local aria_labelledby_count=$(grep '<input' "$1" | grep -c 'aria-labelledby')
|
||||
|
||||
local labeled_count=$((label_count + aria_label_count + aria_labelledby_count))
|
||||
|
||||
if [ "$labeled_count" -ge "$input_count" ]; then
|
||||
print_success "All inputs have labels"
|
||||
else
|
||||
print_error "Some inputs missing labels"
|
||||
echo " Fix: Use <label for=\"id\"> or aria-label"
|
||||
fi
|
||||
|
||||
# Check for required fields
|
||||
if grep -q 'required' "$1"; then
|
||||
if grep -q 'aria-required="true"' "$1"; then
|
||||
print_success "Required fields marked with aria-required"
|
||||
else
|
||||
print_warning "Consider adding aria-required=\"true\" to required fields"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_error_messages() {
|
||||
if grep -q 'aria-describedby' "$1"; then
|
||||
print_success "Error messages linked with aria-describedby"
|
||||
elif grep -q 'error' "$1"; then
|
||||
print_warning "Error handling present, verify aria-describedby usage"
|
||||
fi
|
||||
}
|
||||
|
||||
# Section 4: Interactive Elements
|
||||
print_section "4. INTERACTIVE ELEMENTS"
|
||||
|
||||
check_button_text() {
|
||||
# Check for empty buttons
|
||||
if grep -q '<button[^>]*></button>' "$1"; then
|
||||
print_error "Empty button found"
|
||||
echo " Fix: Add text or aria-label to button"
|
||||
else
|
||||
print_success "No empty buttons found"
|
||||
fi
|
||||
}
|
||||
|
||||
check_link_text() {
|
||||
# Check for generic link text
|
||||
if grep -qi 'click here\|read more\|more' "$1"; then
|
||||
print_warning "Generic link text found (click here, read more)"
|
||||
echo " Tip: Use descriptive link text"
|
||||
else
|
||||
print_success "No generic link text detected"
|
||||
fi
|
||||
}
|
||||
|
||||
check_skip_links() {
|
||||
if grep -q 'skip.*content\|skip.*navigation' "$1"; then
|
||||
print_success "Skip navigation link present"
|
||||
else
|
||||
print_warning "No skip navigation link found"
|
||||
echo " Tip: Add skip link for keyboard users"
|
||||
echo " <a href=\"#main\" class=\"skip-link\">Skip to content</a>"
|
||||
fi
|
||||
}
|
||||
|
||||
# Section 5: ARIA
|
||||
print_section "5. ARIA ATTRIBUTES"
|
||||
|
||||
check_aria_roles() {
|
||||
if grep -q 'role=' "$1"; then
|
||||
print_success "ARIA roles found"
|
||||
|
||||
# Check for button roles on non-button elements
|
||||
if grep -q '<div[^>]*role="button"' "$1" || grep -q '<span[^>]*role="button"' "$1"; then
|
||||
if grep -q 'tabindex=' "$1"; then
|
||||
print_success "Custom buttons have tabindex"
|
||||
else
|
||||
print_error "role=\"button\" without tabindex"
|
||||
echo " Fix: Add tabindex=\"0\" to custom buttons"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_aria_labels() {
|
||||
if grep -q 'aria-label=' "$1"; then
|
||||
print_success "ARIA labels used for context"
|
||||
fi
|
||||
|
||||
# Check for redundant aria-label
|
||||
if grep -q '<button[^>]*aria-label.*>[^<]*</button>' "$1"; then
|
||||
print_warning "Possible redundant aria-label on button with text"
|
||||
echo " Tip: Use aria-label when button has no visible text"
|
||||
fi
|
||||
}
|
||||
|
||||
check_aria_live() {
|
||||
if grep -q 'aria-live' "$1"; then
|
||||
print_success "Live regions defined"
|
||||
fi
|
||||
}
|
||||
|
||||
# Section 6: Keyboard Navigation
|
||||
print_section "6. KEYBOARD NAVIGATION"
|
||||
|
||||
check_tabindex() {
|
||||
# Check for positive tabindex
|
||||
if grep -q 'tabindex="[1-9]' "$1"; then
|
||||
print_error "Positive tabindex values found"
|
||||
echo " Fix: Use tabindex=\"0\" or \"-1\" only"
|
||||
echo " Positive values disrupt natural tab order"
|
||||
else
|
||||
print_success "No positive tabindex values (good)"
|
||||
fi
|
||||
}
|
||||
|
||||
check_focus_indicators() {
|
||||
# This would need CSS analysis
|
||||
print_info "Manual check: Verify focus indicators are visible"
|
||||
echo " Test: Tab through page, ensure focus is visible"
|
||||
echo " CSS: :focus-visible { outline: 2px solid; }"
|
||||
}
|
||||
|
||||
# Section 7: Color & Contrast
|
||||
print_section "7. COLOR & CONTRAST"
|
||||
|
||||
print_info "Manual checks required for color/contrast:"
|
||||
echo ""
|
||||
echo " Required contrast ratios (WCAG AA):"
|
||||
echo " ✓ Normal text: 4.5:1"
|
||||
echo " ✓ Large text (18pt+): 3:1"
|
||||
echo " ✓ UI components: 3:1"
|
||||
echo ""
|
||||
echo " Tools for testing:"
|
||||
echo " - Chrome DevTools (Lighthouse)"
|
||||
echo " - WebAIM Contrast Checker"
|
||||
echo " - axe DevTools"
|
||||
echo ""
|
||||
|
||||
check_color_only() {
|
||||
if grep -qi 'color:.*red\|color:.*green' "$1"; then
|
||||
print_warning "Color usage detected - ensure not used as only indicator"
|
||||
echo " Tip: Don't rely on color alone (add icons, text, patterns)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Section 8: Responsive & Mobile
|
||||
print_section "8. RESPONSIVE & MOBILE"
|
||||
|
||||
check_viewport() {
|
||||
if grep -q 'viewport' "$1"; then
|
||||
print_success "Viewport meta tag present"
|
||||
else
|
||||
print_error "Missing viewport meta tag"
|
||||
echo " Fix: <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
|
||||
fi
|
||||
}
|
||||
|
||||
check_touch_targets() {
|
||||
print_info "Manual check: Touch targets minimum 44x44px"
|
||||
echo " Test: Verify buttons/links meet minimum size"
|
||||
echo " CSS: min-height: 44px; min-width: 44px;"
|
||||
}
|
||||
|
||||
# Section 9: Content
|
||||
print_section "9. CONTENT & READABILITY"
|
||||
|
||||
check_lang_changes() {
|
||||
if grep -q '\slang=' "$1"; then
|
||||
local lang_count=$(grep -o '\slang=' "$1" | wc -l)
|
||||
if [ "$lang_count" -gt 1 ]; then
|
||||
print_success "Language changes marked ($lang_count instances)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_abbreviations() {
|
||||
if grep -q '<abbr' "$1"; then
|
||||
print_success "Abbreviations use <abbr> element"
|
||||
fi
|
||||
}
|
||||
|
||||
# Section 10: Motion & Animation
|
||||
print_section "10. MOTION & ANIMATIONS"
|
||||
|
||||
print_info "Manual check: Respect prefers-reduced-motion"
|
||||
echo ""
|
||||
echo " CSS:"
|
||||
echo " @media (prefers-reduced-motion: reduce) {"
|
||||
echo " * { animation: none !important; }"
|
||||
echo " }"
|
||||
echo ""
|
||||
|
||||
# Run checks on files
|
||||
if [ -f "$TARGET" ]; then
|
||||
# Single file
|
||||
check_html_lang "$TARGET"
|
||||
check_page_title "$TARGET"
|
||||
check_main_landmark "$TARGET"
|
||||
check_heading_structure "$TARGET"
|
||||
check_semantic_html "$TARGET"
|
||||
check_img_alt "$TARGET"
|
||||
check_video_captions "$TARGET"
|
||||
check_form_labels "$TARGET"
|
||||
check_error_messages "$TARGET"
|
||||
check_button_text "$TARGET"
|
||||
check_link_text "$TARGET"
|
||||
check_skip_links "$TARGET"
|
||||
check_aria_roles "$TARGET"
|
||||
check_aria_labels "$TARGET"
|
||||
check_aria_live "$TARGET"
|
||||
check_tabindex "$TARGET"
|
||||
check_focus_indicators "$TARGET"
|
||||
check_color_only "$TARGET"
|
||||
check_viewport "$TARGET"
|
||||
check_touch_targets "$TARGET"
|
||||
check_lang_changes "$TARGET"
|
||||
check_abbreviations "$TARGET"
|
||||
elif [ -d "$TARGET" ]; then
|
||||
# Directory - find HTML files
|
||||
html_files=$(find "$TARGET" -name "*.html" -o -name "*.htm")
|
||||
|
||||
if [ -z "$html_files" ]; then
|
||||
print_error "No HTML files found in $TARGET"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for file in $html_files; do
|
||||
print_info "Checking: $file"
|
||||
check_html_lang "$file"
|
||||
check_page_title "$file"
|
||||
check_main_landmark "$file"
|
||||
check_heading_structure "$file"
|
||||
check_img_alt "$file"
|
||||
check_form_labels "$file"
|
||||
echo ""
|
||||
done
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Audit Summary ║"
|
||||
echo "╚════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo -e "${GREEN}✓ Passed: $PASS_COUNT${NC}"
|
||||
echo -e "${RED}✗ Failed: $FAIL_COUNT${NC}"
|
||||
echo -e "${YELLOW}⚠ Warnings: $WARNING_COUNT${NC}"
|
||||
echo ""
|
||||
|
||||
# Calculate score
|
||||
TOTAL=$((PASS_COUNT + FAIL_COUNT))
|
||||
if [ $TOTAL -gt 0 ]; then
|
||||
SCORE=$(( (PASS_COUNT * 100) / TOTAL ))
|
||||
echo "Score: $SCORE%"
|
||||
echo ""
|
||||
|
||||
if [ $SCORE -ge 90 ]; then
|
||||
echo -e "${GREEN}Excellent! Your site is highly accessible.${NC}"
|
||||
elif [ $SCORE -ge 70 ]; then
|
||||
echo -e "${YELLOW}Good, but needs improvements.${NC}"
|
||||
else
|
||||
echo -e "${RED}Needs significant accessibility improvements.${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_info "Additional Testing Recommended:"
|
||||
echo " 1. Screen reader testing (NVDA, JAWS, VoiceOver)"
|
||||
echo " 2. Keyboard-only navigation"
|
||||
echo " 3. Automated tools (axe, Lighthouse, WAVE)"
|
||||
echo " 4. Color contrast analyzer"
|
||||
echo " 5. Real user testing with assistive technologies"
|
||||
echo ""
|
||||
print_info "Resources:"
|
||||
echo " - WCAG 2.1: https://www.w3.org/WAI/WCAG21/quickref/"
|
||||
echo " - WebAIM: https://webaim.org/"
|
||||
echo " - a11y Project: https://www.a11yproject.com/"
|
||||
echo ""
|
||||
|
||||
# Exit code based on failures
|
||||
if [ $FAIL_COUNT -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
619
skills/frontend-designer/scripts/generate_component.sh
Executable file
619
skills/frontend-designer/scripts/generate_component.sh
Executable file
@@ -0,0 +1,619 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Frontend Designer - Component Generator
|
||||
# Generates accessible, responsive components with design tokens
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper functions
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
prompt_input() {
|
||||
local prompt="$1"
|
||||
local var_name="$2"
|
||||
local required="${3:-false}"
|
||||
|
||||
while true; do
|
||||
echo -e "${BLUE}${prompt}${NC}"
|
||||
read -r input
|
||||
|
||||
if [ -z "$input" ] && [ "$required" = true ]; then
|
||||
print_error "This field is required."
|
||||
continue
|
||||
fi
|
||||
|
||||
eval "$var_name='$input'"
|
||||
break
|
||||
done
|
||||
}
|
||||
|
||||
prompt_select() {
|
||||
local prompt="$1"
|
||||
local var_name="$2"
|
||||
shift 2
|
||||
local options=("$@")
|
||||
|
||||
echo -e "${BLUE}${prompt}${NC}"
|
||||
PS3="Select (1-${#options[@]}): "
|
||||
select opt in "${options[@]}"; do
|
||||
if [ -n "$opt" ]; then
|
||||
eval "$var_name='$opt'"
|
||||
break
|
||||
else
|
||||
print_error "Invalid selection. Try again."
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Banner
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ Frontend Designer - Component Generator ║"
|
||||
echo "║ ║"
|
||||
echo "╚════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Step 1: Component Type
|
||||
print_info "Step 1/6: Component Type"
|
||||
prompt_select "What type of component?" COMPONENT_TYPE \
|
||||
"Button" \
|
||||
"Input" \
|
||||
"Card" \
|
||||
"Modal" \
|
||||
"Dropdown" \
|
||||
"Navigation" \
|
||||
"Form" \
|
||||
"List" \
|
||||
"Custom"
|
||||
|
||||
# Step 2: Component Name
|
||||
print_info "Step 2/6: Component Name"
|
||||
prompt_input "Component name (PascalCase, e.g., UserProfile):" COMPONENT_NAME true
|
||||
|
||||
# Step 3: Framework
|
||||
print_info "Step 3/6: Framework"
|
||||
prompt_select "Which framework?" FRAMEWORK \
|
||||
"React" \
|
||||
"Vue" \
|
||||
"Vanilla JS" \
|
||||
"Web Components"
|
||||
|
||||
# Step 4: Features
|
||||
print_info "Step 4/6: Features (comma-separated)"
|
||||
echo -e "${BLUE}Select features to include (e.g., variants,loading,disabled):${NC}"
|
||||
echo " - variants (different visual styles)"
|
||||
echo " - sizes (sm, md, lg)"
|
||||
echo " - loading (loading state)"
|
||||
echo " - disabled (disabled state)"
|
||||
echo " - icons (icon support)"
|
||||
echo " - responsive (responsive behavior)"
|
||||
read -r FEATURES
|
||||
|
||||
# Step 5: Accessibility
|
||||
print_info "Step 5/6: Accessibility Requirements"
|
||||
prompt_select "WCAG compliance level?" A11Y_LEVEL \
|
||||
"AA (recommended)" \
|
||||
"AAA (strict)" \
|
||||
"Basic"
|
||||
|
||||
# Step 6: Output Directory
|
||||
print_info "Step 6/6: Output Location"
|
||||
prompt_input "Output directory (default: ./components):" OUTPUT_DIR
|
||||
OUTPUT_DIR=${OUTPUT_DIR:-"./components"}
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Generate based on framework
|
||||
case $FRAMEWORK in
|
||||
"React")
|
||||
generate_react_component
|
||||
;;
|
||||
"Vue")
|
||||
generate_vue_component
|
||||
;;
|
||||
"Vanilla JS")
|
||||
generate_vanilla_component
|
||||
;;
|
||||
"Web Components")
|
||||
generate_web_component
|
||||
;;
|
||||
esac
|
||||
|
||||
# Generate component based on selected framework
|
||||
generate_react_component() {
|
||||
local file_path="$OUTPUT_DIR/$COMPONENT_NAME.tsx"
|
||||
|
||||
cat > "$file_path" << 'EOF'
|
||||
import React from 'react';
|
||||
import './COMPONENT_NAME.css';
|
||||
|
||||
interface COMPONENT_NAMEProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
VARIANT_PROP
|
||||
SIZE_PROP
|
||||
DISABLED_PROP
|
||||
LOADING_PROP
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const COMPONENT_NAME: React.FC<COMPONENT_NAMEProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
VARIANT_DEFAULT
|
||||
SIZE_DEFAULT
|
||||
DISABLED_DEFAULT
|
||||
LOADING_DEFAULT
|
||||
onClick,
|
||||
}) => {
|
||||
const baseClass = 'COMPONENT_CLASS';
|
||||
const variantClass = `${baseClass}--${variant}`;
|
||||
const sizeClass = `${baseClass}--${size}`;
|
||||
const classes = `${baseClass} ${variantClass} ${sizeClass} ${className}`;
|
||||
|
||||
return (
|
||||
<COMPONENT_ELEMENT
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
aria-busy={loading}
|
||||
ARIA_ATTRIBUTES
|
||||
>
|
||||
LOADING_SPINNER
|
||||
{children}
|
||||
</COMPONENT_ELEMENT>
|
||||
);
|
||||
};
|
||||
EOF
|
||||
|
||||
# Replace placeholders based on features
|
||||
sed -i "s/COMPONENT_NAME/$COMPONENT_NAME/g" "$file_path"
|
||||
sed -i "s/COMPONENT_CLASS/$(echo "$COMPONENT_NAME" | sed 's/\([A-Z]\)/-\L\1/g' | sed 's/^-//')/g" "$file_path"
|
||||
|
||||
if [[ $FEATURES == *"variants"* ]]; then
|
||||
sed -i "s/VARIANT_PROP/variant?: 'primary' | 'secondary' | 'ghost';/" "$file_path"
|
||||
sed -i "s/VARIANT_DEFAULT/variant = 'primary',/" "$file_path"
|
||||
else
|
||||
sed -i "/VARIANT_PROP/d" "$file_path"
|
||||
sed -i "/VARIANT_DEFAULT/d" "$file_path"
|
||||
fi
|
||||
|
||||
if [[ $FEATURES == *"sizes"* ]]; then
|
||||
sed -i "s/SIZE_PROP/size?: 'sm' | 'md' | 'lg';/" "$file_path"
|
||||
sed -i "s/SIZE_DEFAULT/size = 'md',/" "$file_path"
|
||||
else
|
||||
sed -i "/SIZE_PROP/d" "$file_path"
|
||||
sed -i "/SIZE_DEFAULT/d" "$file_path"
|
||||
fi
|
||||
|
||||
if [[ $FEATURES == *"disabled"* ]]; then
|
||||
sed -i "s/DISABLED_PROP/disabled?: boolean;/" "$file_path"
|
||||
sed -i "s/DISABLED_DEFAULT/disabled = false,/" "$file_path"
|
||||
else
|
||||
sed -i "/DISABLED_PROP/d" "$file_path"
|
||||
sed -i "/DISABLED_DEFAULT/d" "$file_path"
|
||||
fi
|
||||
|
||||
if [[ $FEATURES == *"loading"* ]]; then
|
||||
sed -i "s/LOADING_PROP/loading?: boolean;/" "$file_path"
|
||||
sed -i "s/LOADING_DEFAULT/loading = false,/" "$file_path"
|
||||
sed -i "s|LOADING_SPINNER|{loading \&\& <span className=\"spinner\" aria-hidden=\"true\" />}|" "$file_path"
|
||||
else
|
||||
sed -i "/LOADING_PROP/d" "$file_path"
|
||||
sed -i "/LOADING_DEFAULT/d" "$file_path"
|
||||
sed -i "/LOADING_SPINNER/d" "$file_path"
|
||||
fi
|
||||
|
||||
# Determine element type
|
||||
case $COMPONENT_TYPE in
|
||||
"Button")
|
||||
sed -i "s/COMPONENT_ELEMENT/button/" "$file_path"
|
||||
sed -i "s/ARIA_ATTRIBUTES//" "$file_path"
|
||||
;;
|
||||
"Input")
|
||||
sed -i "s/COMPONENT_ELEMENT/input/" "$file_path"
|
||||
sed -i "s/ARIA_ATTRIBUTES/aria-label=\"\" aria-describedby=\"\"/" "$file_path"
|
||||
;;
|
||||
"Card")
|
||||
sed -i "s/COMPONENT_ELEMENT/div/" "$file_path"
|
||||
sed -i "s/ARIA_ATTRIBUTES/role=\"article\"/" "$file_path"
|
||||
;;
|
||||
*)
|
||||
sed -i "s/COMPONENT_ELEMENT/div/" "$file_path"
|
||||
sed -i "s/ARIA_ATTRIBUTES//" "$file_path"
|
||||
;;
|
||||
esac
|
||||
|
||||
print_success "Created React component: $file_path"
|
||||
generate_css
|
||||
generate_test_file
|
||||
}
|
||||
|
||||
generate_vue_component() {
|
||||
local file_path="$OUTPUT_DIR/$COMPONENT_NAME.vue"
|
||||
|
||||
cat > "$file_path" << 'EOF'
|
||||
<template>
|
||||
<COMPONENT_ELEMENT
|
||||
:class="classes"
|
||||
@click="onClick"
|
||||
:disabled="disabled || loading"
|
||||
:aria-busy="loading"
|
||||
>
|
||||
<span v-if="loading" class="spinner" aria-hidden="true"></span>
|
||||
<slot></slot>
|
||||
</COMPONENT_ELEMENT>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
variant?: 'primary' | 'secondary' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
disabled: false,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [];
|
||||
}>();
|
||||
|
||||
const baseClass = 'COMPONENT_CLASS';
|
||||
|
||||
const classes = computed(() => [
|
||||
baseClass,
|
||||
`${baseClass}--${props.variant}`,
|
||||
`${baseClass}--${props.size}`,
|
||||
]);
|
||||
|
||||
const onClick = () => {
|
||||
if (!props.disabled && !props.loading) {
|
||||
emit('click');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import './COMPONENT_NAME.css';
|
||||
</style>
|
||||
EOF
|
||||
|
||||
sed -i "s/COMPONENT_NAME/$COMPONENT_NAME/g" "$file_path"
|
||||
sed -i "s/COMPONENT_CLASS/$(echo "$COMPONENT_NAME" | sed 's/\([A-Z]\)/-\L\1/g' | sed 's/^-//')/g" "$file_path"
|
||||
|
||||
case $COMPONENT_TYPE in
|
||||
"Button")
|
||||
sed -i "s/COMPONENT_ELEMENT/button/" "$file_path"
|
||||
;;
|
||||
"Input")
|
||||
sed -i "s/COMPONENT_ELEMENT/input/" "$file_path"
|
||||
;;
|
||||
*)
|
||||
sed -i "s/COMPONENT_ELEMENT/div/" "$file_path"
|
||||
;;
|
||||
esac
|
||||
|
||||
print_success "Created Vue component: $file_path"
|
||||
generate_css
|
||||
}
|
||||
|
||||
generate_css() {
|
||||
local css_file="$OUTPUT_DIR/$COMPONENT_NAME.css"
|
||||
local class_name=$(echo "$COMPONENT_NAME" | sed 's/\([A-Z]\)/-\L\1/g' | sed 's/^-//')
|
||||
|
||||
cat > "$css_file" << EOF
|
||||
/* $COMPONENT_NAME Component Styles */
|
||||
|
||||
.$class_name {
|
||||
/* Design Tokens */
|
||||
--component-bg: var(--color-surface);
|
||||
--component-text: var(--color-text);
|
||||
--component-border: var(--color-border);
|
||||
--component-radius: var(--radius-md);
|
||||
--component-shadow: var(--shadow-sm);
|
||||
|
||||
/* Base Styles */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
|
||||
padding: var(--space-3) var(--space-4);
|
||||
|
||||
background-color: var(--component-bg);
|
||||
color: var(--component-text);
|
||||
border: 1px solid var(--component-border);
|
||||
border-radius: var(--component-radius);
|
||||
|
||||
font-family: var(--font-base);
|
||||
font-size: var(--text-base);
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
transition: all 0.2s ease;
|
||||
|
||||
/* Accessibility */
|
||||
min-height: 44px; /* WCAG touch target */
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* Variants */
|
||||
.$class_name--primary {
|
||||
--component-bg: var(--color-primary);
|
||||
--component-text: var(--color-white);
|
||||
--component-border: var(--color-primary);
|
||||
}
|
||||
|
||||
.$class_name--primary:hover:not(:disabled) {
|
||||
--component-bg: var(--color-primary-hover);
|
||||
--component-border: var(--color-primary-hover);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.$class_name--secondary {
|
||||
--component-bg: transparent;
|
||||
--component-text: var(--color-primary);
|
||||
--component-border: var(--color-primary);
|
||||
}
|
||||
|
||||
.$class_name--secondary:hover:not(:disabled) {
|
||||
--component-bg: var(--color-primary-subtle);
|
||||
}
|
||||
|
||||
.$class_name--ghost {
|
||||
--component-bg: transparent;
|
||||
--component-text: var(--color-text);
|
||||
--component-border: transparent;
|
||||
}
|
||||
|
||||
.$class_name--ghost:hover:not(:disabled) {
|
||||
--component-bg: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
.$class_name--sm {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.$class_name--md {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-size: var(--text-base);
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.$class_name--lg {
|
||||
padding: var(--space-4) var(--space-6);
|
||||
font-size: var(--text-lg);
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
/* States */
|
||||
.$class_name:focus-visible {
|
||||
outline: 2px solid var(--color-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.$class_name:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.$class_name:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.$class_name[aria-busy="true"] {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.$class_name {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.$class_name {
|
||||
--component-bg: var(--color-surface-dark);
|
||||
--component-text: var(--color-text-dark);
|
||||
--component-border: var(--color-border-dark);
|
||||
}
|
||||
}
|
||||
|
||||
/* High Contrast Mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.$class_name {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.$class_name:focus-visible {
|
||||
outline-width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced Motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.$class_name,
|
||||
.spinner {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
print_success "Created CSS file: $css_file"
|
||||
}
|
||||
|
||||
generate_test_file() {
|
||||
if [ "$FRAMEWORK" != "React" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local test_file="$OUTPUT_DIR/$COMPONENT_NAME.test.tsx"
|
||||
|
||||
cat > "$test_file" << 'EOF'
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { COMPONENT_NAME } from './COMPONENT_NAME';
|
||||
|
||||
describe('COMPONENT_NAME', () => {
|
||||
it('renders children correctly', () => {
|
||||
render(<COMPONENT_NAME>Click me</COMPONENT_NAME>);
|
||||
expect(screen.getByText('Click me')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles click events', () => {
|
||||
const handleClick = jest.fn();
|
||||
render(<COMPONENT_NAME onClick={handleClick}>Click me</COMPONENT_NAME>);
|
||||
|
||||
fireEvent.click(screen.getByText('Click me'));
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders with different variants', () => {
|
||||
const { rerender } = render(<COMPONENT_NAME variant="primary">Primary</COMPONENT_NAME>);
|
||||
expect(screen.getByText('Primary')).toHaveClass('COMPONENT_CLASS--primary');
|
||||
|
||||
rerender(<COMPONENT_NAME variant="secondary">Secondary</COMPONENT_NAME>);
|
||||
expect(screen.getByText('Secondary')).toHaveClass('COMPONENT_CLASS--secondary');
|
||||
});
|
||||
|
||||
it('renders with different sizes', () => {
|
||||
const { rerender } = render(<COMPONENT_NAME size="sm">Small</COMPONENT_NAME>);
|
||||
expect(screen.getByText('Small')).toHaveClass('COMPONENT_CLASS--sm');
|
||||
|
||||
rerender(<COMPONENT_NAME size="lg">Large</COMPONENT_NAME>);
|
||||
expect(screen.getByText('Large')).toHaveClass('COMPONENT_CLASS--lg');
|
||||
});
|
||||
|
||||
it('disables interaction when disabled', () => {
|
||||
const handleClick = jest.fn();
|
||||
render(<COMPONENT_NAME disabled onClick={handleClick}>Disabled</COMPONENT_NAME>);
|
||||
|
||||
const element = screen.getByText('Disabled');
|
||||
expect(element).toBeDisabled();
|
||||
|
||||
fireEvent.click(element);
|
||||
expect(handleClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows loading state', () => {
|
||||
render(<COMPONENT_NAME loading>Loading</COMPONENT_NAME>);
|
||||
|
||||
const element = screen.getByText('Loading');
|
||||
expect(element).toHaveAttribute('aria-busy', 'true');
|
||||
expect(element).toBeDisabled();
|
||||
});
|
||||
|
||||
it('is keyboard accessible', () => {
|
||||
const handleClick = jest.fn();
|
||||
render(<COMPONENT_NAME onClick={handleClick}>Accessible</COMPONENT_NAME>);
|
||||
|
||||
const element = screen.getByText('Accessible');
|
||||
element.focus();
|
||||
expect(element).toHaveFocus();
|
||||
});
|
||||
|
||||
it('has proper ARIA attributes', () => {
|
||||
render(<COMPONENT_NAME loading>ARIA Test</COMPONENT_NAME>);
|
||||
const element = screen.getByText('ARIA Test');
|
||||
expect(element).toHaveAttribute('aria-busy', 'true');
|
||||
});
|
||||
});
|
||||
EOF
|
||||
|
||||
sed -i "s/COMPONENT_NAME/$COMPONENT_NAME/g" "$test_file"
|
||||
sed -i "s/COMPONENT_CLASS/$(echo "$COMPONENT_NAME" | sed 's/\([A-Z]\)/-\L\1/g' | sed 's/^-//')/g" "$test_file"
|
||||
|
||||
print_success "Created test file: $test_file"
|
||||
}
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Generation Complete ║"
|
||||
echo "╚════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
print_success "Component: $COMPONENT_NAME"
|
||||
print_success "Type: $COMPONENT_TYPE"
|
||||
print_success "Framework: $FRAMEWORK"
|
||||
print_success "Location: $OUTPUT_DIR"
|
||||
echo ""
|
||||
print_info "Files created:"
|
||||
case $FRAMEWORK in
|
||||
"React")
|
||||
echo " - $COMPONENT_NAME.tsx (component)"
|
||||
echo " - $COMPONENT_NAME.css (styles)"
|
||||
echo " - $COMPONENT_NAME.test.tsx (tests)"
|
||||
;;
|
||||
"Vue")
|
||||
echo " - $COMPONENT_NAME.vue (component)"
|
||||
echo " - $COMPONENT_NAME.css (styles)"
|
||||
;;
|
||||
*)
|
||||
echo " - $COMPONENT_NAME.js (component)"
|
||||
echo " - $COMPONENT_NAME.css (styles)"
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
print_info "Next steps:"
|
||||
echo " 1. Review generated files"
|
||||
echo " 2. Customize component logic"
|
||||
echo " 3. Add to your component library"
|
||||
echo " 4. Run tests (npm test)"
|
||||
echo " 5. Test accessibility (npm run a11y)"
|
||||
echo ""
|
||||
570
skills/frontend-designer/scripts/setup_design_system.sh
Executable file
570
skills/frontend-designer/scripts/setup_design_system.sh
Executable file
@@ -0,0 +1,570 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Frontend Designer - Design System Setup
|
||||
# Initialize a complete design system with tokens, components, and documentation
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Helper functions
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
prompt_input() {
|
||||
local prompt="$1"
|
||||
local var_name="$2"
|
||||
local default="${3:-}"
|
||||
|
||||
echo -e "${BLUE}${prompt}${NC}"
|
||||
if [ -n "$default" ]; then
|
||||
echo -e "${YELLOW}(default: $default)${NC}"
|
||||
fi
|
||||
read -r input
|
||||
|
||||
if [ -z "$input" ]; then
|
||||
input="$default"
|
||||
fi
|
||||
|
||||
eval "$var_name='$input'"
|
||||
}
|
||||
|
||||
prompt_confirm() {
|
||||
local prompt="$1"
|
||||
echo -e "${BLUE}${prompt} (y/n):${NC}"
|
||||
read -r response
|
||||
[[ "$response" =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
# Banner
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ Frontend Designer - Design System Setup ║"
|
||||
echo "║ ║"
|
||||
echo "╚════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Configuration
|
||||
print_info "Step 1/5: Project Configuration"
|
||||
prompt_input "Design system name:" DS_NAME "design-system"
|
||||
prompt_input "Primary brand color (hex):" PRIMARY_COLOR "#0066FF"
|
||||
prompt_input "Base font size (px):" BASE_FONT_SIZE "16"
|
||||
prompt_input "Output directory:" OUTPUT_DIR "./design-system"
|
||||
|
||||
# Create directory structure
|
||||
print_info "Step 2/5: Creating Directory Structure"
|
||||
mkdir -p "$OUTPUT_DIR"/{tokens,components,utilities,docs,examples}
|
||||
print_success "Created directory structure"
|
||||
|
||||
# Generate design tokens
|
||||
print_info "Step 3/5: Generating Design Tokens"
|
||||
generate_design_tokens
|
||||
|
||||
# Generate base components
|
||||
print_info "Step 4/5: Generating Base Components"
|
||||
if prompt_confirm "Generate base component library?"; then
|
||||
generate_base_components
|
||||
fi
|
||||
|
||||
# Generate documentation
|
||||
print_info "Step 5/5: Generating Documentation"
|
||||
generate_documentation
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Setup Complete! ║"
|
||||
echo "╚════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
print_success "Design system created: $OUTPUT_DIR"
|
||||
echo ""
|
||||
print_info "Next steps:"
|
||||
echo " 1. Review tokens in $OUTPUT_DIR/tokens/"
|
||||
echo " 2. Customize brand colors and typography"
|
||||
echo " 3. Import tokens in your app"
|
||||
echo " 4. Use components from $OUTPUT_DIR/components/"
|
||||
echo " 5. Read documentation in $OUTPUT_DIR/docs/"
|
||||
echo ""
|
||||
|
||||
generate_design_tokens() {
|
||||
# Color tokens
|
||||
cat > "$OUTPUT_DIR/tokens/colors.css" << EOF
|
||||
/**
|
||||
* Color Tokens
|
||||
* Generated by Frontend Designer
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Brand Colors */
|
||||
--color-primary: $PRIMARY_COLOR;
|
||||
--color-primary-hover: color-mix(in srgb, var(--color-primary) 85%, black);
|
||||
--color-primary-active: color-mix(in srgb, var(--color-primary) 75%, black);
|
||||
--color-primary-subtle: color-mix(in srgb, var(--color-primary) 10%, white);
|
||||
|
||||
--color-secondary: #6B7280;
|
||||
--color-secondary-hover: #4B5563;
|
||||
--color-secondary-subtle: #F3F4F6;
|
||||
|
||||
/* Semantic Colors */
|
||||
--color-success: #10B981;
|
||||
--color-success-hover: #059669;
|
||||
--color-success-subtle: #D1FAE5;
|
||||
|
||||
--color-warning: #F59E0B;
|
||||
--color-warning-hover: #D97706;
|
||||
--color-warning-subtle: #FEF3C7;
|
||||
|
||||
--color-error: #EF4444;
|
||||
--color-error-hover: #DC2626;
|
||||
--color-error-subtle: #FEE2E2;
|
||||
|
||||
--color-info: #3B82F6;
|
||||
--color-info-hover: #2563EB;
|
||||
--color-info-subtle: #DBEAFE;
|
||||
|
||||
/* Neutral Colors */
|
||||
--color-white: #FFFFFF;
|
||||
--color-black: #000000;
|
||||
|
||||
--color-gray-50: #F9FAFB;
|
||||
--color-gray-100: #F3F4F6;
|
||||
--color-gray-200: #E5E7EB;
|
||||
--color-gray-300: #D1D5DB;
|
||||
--color-gray-400: #9CA3AF;
|
||||
--color-gray-500: #6B7280;
|
||||
--color-gray-600: #4B5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1F2937;
|
||||
--color-gray-900: #111827;
|
||||
|
||||
/* Surface Colors */
|
||||
--color-surface: var(--color-white);
|
||||
--color-surface-hover: var(--color-gray-50);
|
||||
--color-surface-subtle: var(--color-gray-100);
|
||||
|
||||
--color-background: var(--color-white);
|
||||
--color-background-alt: var(--color-gray-50);
|
||||
|
||||
/* Text Colors */
|
||||
--color-text: var(--color-gray-900);
|
||||
--color-text-secondary: var(--color-gray-600);
|
||||
--color-text-tertiary: var(--color-gray-400);
|
||||
--color-text-inverse: var(--color-white);
|
||||
|
||||
/* Border Colors */
|
||||
--color-border: var(--color-gray-200);
|
||||
--color-border-hover: var(--color-gray-300);
|
||||
--color-border-focus: var(--color-primary);
|
||||
|
||||
/* Focus Ring */
|
||||
--color-focus: var(--color-primary);
|
||||
--color-focus-ring: color-mix(in srgb, var(--color-primary) 50%, transparent);
|
||||
}
|
||||
|
||||
/* Dark Mode */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-surface: var(--color-gray-800);
|
||||
--color-surface-hover: var(--color-gray-700);
|
||||
--color-surface-subtle: var(--color-gray-900);
|
||||
|
||||
--color-background: var(--color-gray-900);
|
||||
--color-background-alt: var(--color-gray-800);
|
||||
|
||||
--color-text: var(--color-gray-50);
|
||||
--color-text-secondary: var(--color-gray-300);
|
||||
--color-text-tertiary: var(--color-gray-500);
|
||||
|
||||
--color-border: var(--color-gray-700);
|
||||
--color-border-hover: var(--color-gray-600);
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Typography tokens
|
||||
cat > "$OUTPUT_DIR/tokens/typography.css" << EOF
|
||||
/**
|
||||
* Typography Tokens
|
||||
* Generated by Frontend Designer
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Font Families */
|
||||
--font-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-heading: var(--font-base);
|
||||
--font-mono: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
|
||||
|
||||
/* Font Sizes - Fluid Typography */
|
||||
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
|
||||
--text-sm: clamp(0.875rem, 0.825rem + 0.25vw, 1rem);
|
||||
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
|
||||
--text-lg: clamp(1.125rem, 1.05rem + 0.375vw, 1.25rem);
|
||||
--text-xl: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
|
||||
--text-2xl: clamp(1.5rem, 1.35rem + 0.75vw, 1.875rem);
|
||||
--text-3xl: clamp(1.875rem, 1.65rem + 1.125vw, 2.25rem);
|
||||
--text-4xl: clamp(2.25rem, 1.95rem + 1.5vw, 3rem);
|
||||
--text-5xl: clamp(3rem, 2.55rem + 2.25vw, 3.75rem);
|
||||
|
||||
/* Font Weights */
|
||||
--font-light: 300;
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
--font-extrabold: 800;
|
||||
|
||||
/* Line Heights */
|
||||
--leading-none: 1;
|
||||
--leading-tight: 1.25;
|
||||
--leading-snug: 1.375;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.625;
|
||||
--leading-loose: 2;
|
||||
|
||||
/* Letter Spacing */
|
||||
--tracking-tighter: -0.05em;
|
||||
--tracking-tight: -0.025em;
|
||||
--tracking-normal: 0;
|
||||
--tracking-wide: 0.025em;
|
||||
--tracking-wider: 0.05em;
|
||||
--tracking-widest: 0.1em;
|
||||
}
|
||||
EOF
|
||||
|
||||
# Spacing tokens
|
||||
cat > "$OUTPUT_DIR/tokens/spacing.css" << EOF
|
||||
/**
|
||||
* Spacing Tokens
|
||||
* Generated by Frontend Designer
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Base spacing scale */
|
||||
--space-0: 0;
|
||||
--space-px: 1px;
|
||||
--space-0-5: 0.125rem; /* 2px */
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-1-5: 0.375rem; /* 6px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-2-5: 0.625rem; /* 10px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-3-5: 0.875rem; /* 14px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-5: 1.25rem; /* 20px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-7: 1.75rem; /* 28px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-9: 2.25rem; /* 36px */
|
||||
--space-10: 2.5rem; /* 40px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
--space-14: 3.5rem; /* 56px */
|
||||
--space-16: 4rem; /* 64px */
|
||||
--space-20: 5rem; /* 80px */
|
||||
--space-24: 6rem; /* 96px */
|
||||
--space-32: 8rem; /* 128px */
|
||||
|
||||
/* Container widths */
|
||||
--container-xs: 20rem; /* 320px */
|
||||
--container-sm: 24rem; /* 384px */
|
||||
--container-md: 28rem; /* 448px */
|
||||
--container-lg: 32rem; /* 512px */
|
||||
--container-xl: 36rem; /* 576px */
|
||||
--container-2xl: 42rem; /* 672px */
|
||||
--container-3xl: 48rem; /* 768px */
|
||||
--container-4xl: 56rem; /* 896px */
|
||||
--container-5xl: 64rem; /* 1024px */
|
||||
--container-6xl: 72rem; /* 1152px */
|
||||
--container-7xl: 80rem; /* 1280px */
|
||||
}
|
||||
EOF
|
||||
|
||||
# Shadow tokens
|
||||
cat > "$OUTPUT_DIR/tokens/shadows.css" << EOF
|
||||
/**
|
||||
* Shadow Tokens
|
||||
* Generated by Frontend Designer
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Box Shadows */
|
||||
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
|
||||
/* Inner Shadow */
|
||||
--shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* Focus Shadow */
|
||||
--shadow-focus: 0 0 0 3px var(--color-focus-ring);
|
||||
}
|
||||
|
||||
/* Dark Mode Shadows */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.4);
|
||||
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.5), 0 1px 2px 0 rgba(0, 0, 0, 0.4);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.4);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Border radius tokens
|
||||
cat > "$OUTPUT_DIR/tokens/borders.css" << EOF
|
||||
/**
|
||||
* Border Tokens
|
||||
* Generated by Frontend Designer
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Border Radius */
|
||||
--radius-none: 0;
|
||||
--radius-sm: 0.125rem; /* 2px */
|
||||
--radius-md: 0.375rem; /* 6px */
|
||||
--radius-lg: 0.5rem; /* 8px */
|
||||
--radius-xl: 0.75rem; /* 12px */
|
||||
--radius-2xl: 1rem; /* 16px */
|
||||
--radius-3xl: 1.5rem; /* 24px */
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Border Widths */
|
||||
--border-0: 0;
|
||||
--border-1: 1px;
|
||||
--border-2: 2px;
|
||||
--border-4: 4px;
|
||||
--border-8: 8px;
|
||||
}
|
||||
EOF
|
||||
|
||||
# Z-index tokens
|
||||
cat > "$OUTPUT_DIR/tokens/zindex.css" << EOF
|
||||
/**
|
||||
* Z-Index Tokens
|
||||
* Generated by Frontend Designer
|
||||
*/
|
||||
|
||||
:root {
|
||||
--z-0: 0;
|
||||
--z-10: 10;
|
||||
--z-20: 20;
|
||||
--z-30: 30;
|
||||
--z-40: 40;
|
||||
--z-50: 50;
|
||||
|
||||
/* Semantic z-index */
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1020;
|
||||
--z-fixed: 1030;
|
||||
--z-modal-backdrop: 1040;
|
||||
--z-modal: 1050;
|
||||
--z-popover: 1060;
|
||||
--z-tooltip: 1070;
|
||||
}
|
||||
EOF
|
||||
|
||||
# Main tokens file
|
||||
cat > "$OUTPUT_DIR/tokens/index.css" << EOF
|
||||
/**
|
||||
* Design Tokens - $DS_NAME
|
||||
* Generated by Frontend Designer
|
||||
*
|
||||
* Import this file in your app to use all design tokens
|
||||
*/
|
||||
|
||||
@import './colors.css';
|
||||
@import './typography.css';
|
||||
@import './spacing.css';
|
||||
@import './shadows.css';
|
||||
@import './borders.css';
|
||||
@import './zindex.css';
|
||||
|
||||
/* Base Reset */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: ${BASE_FONT_SIZE}px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-base);
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--leading-normal);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Reduced Motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
print_success "Generated design tokens"
|
||||
}
|
||||
|
||||
generate_base_components() {
|
||||
# This would generate base components
|
||||
# For now, just create placeholder
|
||||
cat > "$OUTPUT_DIR/components/README.md" << EOF
|
||||
# Components
|
||||
|
||||
Base component library for $DS_NAME.
|
||||
|
||||
## Usage
|
||||
|
||||
Import components from this directory:
|
||||
|
||||
\`\`\`tsx
|
||||
import { Button } from './components/Button';
|
||||
import { Card } from './components/Card';
|
||||
\`\`\`
|
||||
|
||||
## Available Components
|
||||
|
||||
- Button (multiple variants and sizes)
|
||||
- Card (container component)
|
||||
- Input (form input with validation)
|
||||
- Modal (accessible dialog)
|
||||
- Dropdown (accessible select)
|
||||
|
||||
Generate components using:
|
||||
|
||||
\`\`\`bash
|
||||
./scripts/generate_component.sh
|
||||
\`\`\`
|
||||
EOF
|
||||
|
||||
print_success "Created components directory"
|
||||
}
|
||||
|
||||
generate_documentation() {
|
||||
cat > "$OUTPUT_DIR/docs/README.md" << EOF
|
||||
# $DS_NAME Documentation
|
||||
|
||||
Complete design system documentation and guidelines.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Install design tokens**
|
||||
|
||||
\`\`\`css
|
||||
@import 'design-system/tokens/index.css';
|
||||
\`\`\`
|
||||
|
||||
2. **Use design tokens**
|
||||
|
||||
\`\`\`css
|
||||
.my-component {
|
||||
color: var(--color-primary);
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
3. **Import components**
|
||||
|
||||
\`\`\`tsx
|
||||
import { Button } from 'design-system/components';
|
||||
\`\`\`
|
||||
|
||||
## Design Tokens
|
||||
|
||||
### Colors
|
||||
|
||||
- **Brand**: \`--color-primary\`, \`--color-secondary\`
|
||||
- **Semantic**: \`--color-success\`, \`--color-warning\`, \`--color-error\`
|
||||
- **Neutral**: \`--color-gray-*\` (50-900)
|
||||
|
||||
### Typography
|
||||
|
||||
- **Font families**: \`--font-base\`, \`--font-heading\`, \`--font-mono\`
|
||||
- **Sizes**: \`--text-xs\` through \`--text-5xl\`
|
||||
- **Weights**: \`--font-light\` through \`--font-extrabold\`
|
||||
|
||||
### Spacing
|
||||
|
||||
- **Scale**: \`--space-0\` through \`--space-32\`
|
||||
- **Containers**: \`--container-xs\` through \`--container-7xl\`
|
||||
|
||||
### Shadows
|
||||
|
||||
- **Elevation**: \`--shadow-xs\` through \`--shadow-2xl\`
|
||||
- **Focus**: \`--shadow-focus\`
|
||||
|
||||
### Border Radius
|
||||
|
||||
- **Sizes**: \`--radius-sm\` through \`--radius-3xl\`
|
||||
- **Full**: \`--radius-full\` (pills/circles)
|
||||
|
||||
## Accessibility
|
||||
|
||||
This design system follows WCAG 2.1 AA guidelines:
|
||||
|
||||
- ✅ Color contrast ratios: 4.5:1 for text, 3:1 for UI
|
||||
- ✅ Touch targets: 44x44px minimum
|
||||
- ✅ Keyboard navigation support
|
||||
- ✅ Focus indicators
|
||||
- ✅ Reduced motion support
|
||||
- ✅ Dark mode support
|
||||
- ✅ High contrast mode support
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Chrome (last 2 versions)
|
||||
- Firefox (last 2 versions)
|
||||
- Safari (last 2 versions)
|
||||
- Edge (last 2 versions)
|
||||
|
||||
## Contributing
|
||||
|
||||
See CONTRIBUTING.md for contribution guidelines.
|
||||
EOF
|
||||
|
||||
print_success "Created documentation"
|
||||
}
|
||||
Reference in New Issue
Block a user