Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:48:55 +08:00
commit f28999f19c
127 changed files with 62038 additions and 0 deletions

View 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

View 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 ""

View 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"
}