Initial commit
This commit is contained in:
492
skills/frontend-component/SKILL.md
Normal file
492
skills/frontend-component/SKILL.md
Normal file
@@ -0,0 +1,492 @@
|
||||
---
|
||||
name: frontend-component
|
||||
description: Create React/Vue component with TypeScript, tests, and styles. Auto-invoke when user says "create component", "add component", "new component", or "build component".
|
||||
allowed-tools: Read, Write, Edit, Grep, Glob, Bash
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Frontend Component Generator
|
||||
|
||||
Generate production-ready React/Vue components with TypeScript, tests, and styles following modern best practices.
|
||||
|
||||
## When to Invoke
|
||||
|
||||
Auto-invoke when user mentions:
|
||||
- "Create a component"
|
||||
- "Add a component"
|
||||
- "New component"
|
||||
- "Build a component"
|
||||
- "Generate component for [feature]"
|
||||
|
||||
## What This Does
|
||||
|
||||
1. Generates component file with TypeScript and props interface
|
||||
2. Creates test file with React Testing Library
|
||||
3. Generates CSS module for styling
|
||||
4. Creates barrel export (index.ts)
|
||||
5. Validates naming conventions
|
||||
6. Follows project patterns
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: Gather Component Requirements
|
||||
|
||||
**Ask user for component details**:
|
||||
```
|
||||
Component name: [PascalCase name, e.g., UserProfile]
|
||||
Component type:
|
||||
- simple (basic functional component)
|
||||
- with-hooks (useState, useEffect, etc.)
|
||||
- container (data fetching component)
|
||||
|
||||
Styling approach:
|
||||
- css-modules (default)
|
||||
- styled-components
|
||||
- tailwind
|
||||
|
||||
Props needed: [Optional: describe expected props]
|
||||
```
|
||||
|
||||
**Validate component name**:
|
||||
- Use predefined function: `functions/name_validator.py`
|
||||
- Ensure PascalCase format
|
||||
- No reserved words
|
||||
- Descriptive and specific
|
||||
|
||||
### Step 2: Generate Props Interface
|
||||
|
||||
**Based on component type and requirements**:
|
||||
|
||||
Use predefined function: `functions/props_interface_generator.py`
|
||||
|
||||
```python
|
||||
# Generates TypeScript interface based on component requirements
|
||||
python3 functions/props_interface_generator.py \
|
||||
--name "UserProfile" \
|
||||
--props "userId:string,onUpdate:function,isActive:boolean"
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```typescript
|
||||
interface UserProfileProps {
|
||||
userId: string;
|
||||
onUpdate?: () => void;
|
||||
isActive?: boolean;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Generate Component File
|
||||
|
||||
**Use appropriate template based on type**:
|
||||
|
||||
**Simple component**:
|
||||
```
|
||||
Use template: templates/component-simple-template.tsx
|
||||
```
|
||||
|
||||
**Component with hooks**:
|
||||
```
|
||||
Use template: templates/component-with-hooks-template.tsx
|
||||
```
|
||||
|
||||
**Container component**:
|
||||
```
|
||||
Use template: templates/component-container-template.tsx
|
||||
```
|
||||
|
||||
**Use predefined function**: `functions/component_generator.py`
|
||||
|
||||
```bash
|
||||
python3 functions/component_generator.py \
|
||||
--name "UserProfile" \
|
||||
--type "simple" \
|
||||
--props-interface "UserProfileProps" \
|
||||
--template "templates/component-simple-template.tsx" \
|
||||
--output "src/components/UserProfile/UserProfile.tsx"
|
||||
```
|
||||
|
||||
**Template substitutions**:
|
||||
- `${COMPONENT_NAME}` → Component name (PascalCase)
|
||||
- `${PROPS_INTERFACE}` → Generated props interface
|
||||
- `${STYLE_IMPORT}` → CSS module import
|
||||
- `${DESCRIPTION}` → Brief component description
|
||||
|
||||
### Step 4: Generate Test File
|
||||
|
||||
**Use predefined function**: `functions/test_generator.py`
|
||||
|
||||
```bash
|
||||
python3 functions/test_generator.py \
|
||||
--component-name "UserProfile" \
|
||||
--component-path "src/components/UserProfile/UserProfile.tsx" \
|
||||
--template "templates/test-template.test.tsx" \
|
||||
--output "src/components/UserProfile/UserProfile.test.tsx"
|
||||
```
|
||||
|
||||
**Test template includes**:
|
||||
- Basic rendering test
|
||||
- Props validation test
|
||||
- Event handler tests (if applicable)
|
||||
- Accessibility tests
|
||||
|
||||
**Template substitutions**:
|
||||
- `${COMPONENT_NAME}` → Component name
|
||||
- `${IMPORT_PATH}` → Relative import path
|
||||
- `${TEST_CASES}` → Generated test cases based on props
|
||||
|
||||
### Step 5: Generate Style File
|
||||
|
||||
**Use predefined function**: `functions/style_generator.py`
|
||||
|
||||
```bash
|
||||
python3 functions/style_generator.py \
|
||||
--name "UserProfile" \
|
||||
--approach "css-modules" \
|
||||
--template "templates/style-template.module.css" \
|
||||
--output "src/components/UserProfile/UserProfile.module.css"
|
||||
```
|
||||
|
||||
**CSS Modules template**:
|
||||
```css
|
||||
.container {
|
||||
/* Component wrapper styles */
|
||||
}
|
||||
|
||||
.title {
|
||||
/* Title styles */
|
||||
}
|
||||
|
||||
/* Add more classes as needed */
|
||||
```
|
||||
|
||||
**Styled Components alternative**:
|
||||
```typescript
|
||||
// Generated if --approach "styled-components"
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
/* Component wrapper styles */
|
||||
`;
|
||||
|
||||
export const Title = styled.h2`
|
||||
/* Title styles */
|
||||
`;
|
||||
```
|
||||
|
||||
### Step 6: Generate Barrel Export
|
||||
|
||||
**Create index.ts for clean imports**:
|
||||
|
||||
```bash
|
||||
Write(
|
||||
file_path: "src/components/UserProfile/index.ts",
|
||||
content: "export { UserProfile } from './UserProfile';\nexport type { UserProfileProps } from './UserProfile';\n"
|
||||
)
|
||||
```
|
||||
|
||||
**Allows usage**:
|
||||
```typescript
|
||||
import { UserProfile } from '@/components/UserProfile';
|
||||
```
|
||||
|
||||
### Step 7: Show Component Summary
|
||||
|
||||
**Display generated files and usage**:
|
||||
|
||||
```
|
||||
✅ Component Created: UserProfile
|
||||
|
||||
Structure:
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📁 src/components/UserProfile/
|
||||
├── UserProfile.tsx (Component)
|
||||
├── UserProfile.test.tsx (Tests)
|
||||
├── UserProfile.module.css (Styles)
|
||||
└── index.ts (Exports)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Props Interface:
|
||||
interface UserProfileProps {
|
||||
userId: string;
|
||||
onUpdate?: () => void;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
Usage:
|
||||
import { UserProfile } from '@/components/UserProfile';
|
||||
|
||||
<UserProfile
|
||||
userId="123"
|
||||
onUpdate={() => console.log('Updated')}
|
||||
isActive={true}
|
||||
/>
|
||||
|
||||
Next Steps:
|
||||
1. Customize component implementation
|
||||
2. Run tests: npm test UserProfile
|
||||
3. Import and use in your feature
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Predefined Functions
|
||||
|
||||
### 1. name_validator.py
|
||||
|
||||
Validates component naming conventions.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
python3 functions/name_validator.py --name "UserProfile"
|
||||
```
|
||||
|
||||
**Checks**:
|
||||
- PascalCase format
|
||||
- Not a reserved word (e.g., Component, Element, etc.)
|
||||
- Descriptive (length > 2 chars)
|
||||
- No special characters
|
||||
|
||||
**Returns**: Valid name or error message
|
||||
|
||||
---
|
||||
|
||||
### 2. props_interface_generator.py
|
||||
|
||||
Generates TypeScript props interface from user input.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
python3 functions/props_interface_generator.py \
|
||||
--name "UserProfile" \
|
||||
--props "userId:string,onUpdate:function,isActive:boolean"
|
||||
```
|
||||
|
||||
**Supported types**:
|
||||
- `string`, `number`, `boolean`
|
||||
- `function` (becomes `() => void`)
|
||||
- `array` (becomes `any[]`)
|
||||
- `object` (becomes `Record<string, any>`)
|
||||
- `react-node` (becomes `React.ReactNode`)
|
||||
|
||||
**Returns**: TypeScript interface string
|
||||
|
||||
---
|
||||
|
||||
### 3. component_generator.py
|
||||
|
||||
Generates component file from template with substitutions.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
python3 functions/component_generator.py \
|
||||
--name "UserProfile" \
|
||||
--type "simple" \
|
||||
--props-interface "UserProfileProps" \
|
||||
--template "templates/component-simple-template.tsx" \
|
||||
--output "src/components/UserProfile/UserProfile.tsx"
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `--name`: Component name (PascalCase)
|
||||
- `--type`: Component type (simple/with-hooks/container)
|
||||
- `--props-interface`: Props interface name
|
||||
- `--template`: Template file path
|
||||
- `--output`: Output file path
|
||||
|
||||
**Returns**: Generated component code
|
||||
|
||||
---
|
||||
|
||||
### 4. test_generator.py
|
||||
|
||||
Generates test file with React Testing Library.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
python3 functions/test_generator.py \
|
||||
--component-name "UserProfile" \
|
||||
--component-path "src/components/UserProfile/UserProfile.tsx" \
|
||||
--template "templates/test-template.test.tsx" \
|
||||
--output "src/components/UserProfile/UserProfile.test.tsx"
|
||||
```
|
||||
|
||||
**Generates tests for**:
|
||||
- Component rendering
|
||||
- Props validation
|
||||
- Event handlers
|
||||
- Accessibility attributes
|
||||
|
||||
**Returns**: Generated test code
|
||||
|
||||
---
|
||||
|
||||
### 5. style_generator.py
|
||||
|
||||
Generates style file (CSS Modules or Styled Components).
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
python3 functions/style_generator.py \
|
||||
--name "UserProfile" \
|
||||
--approach "css-modules" \
|
||||
--template "templates/style-template.module.css" \
|
||||
--output "src/components/UserProfile/UserProfile.module.css"
|
||||
```
|
||||
|
||||
**Supported approaches**:
|
||||
- `css-modules` (default)
|
||||
- `styled-components`
|
||||
- `tailwind` (generates className utilities)
|
||||
|
||||
**Returns**: Generated style code
|
||||
|
||||
---
|
||||
|
||||
## Templates
|
||||
|
||||
### component-simple-template.tsx
|
||||
|
||||
Basic functional component template.
|
||||
|
||||
**Placeholders**:
|
||||
- `${COMPONENT_NAME}` - Component name
|
||||
- `${PROPS_INTERFACE}` - Props interface definition
|
||||
- `${STYLE_IMPORT}` - CSS import statement
|
||||
- `${DESCRIPTION}` - Component description
|
||||
|
||||
### component-with-hooks-template.tsx
|
||||
|
||||
Component template with useState, useEffect examples.
|
||||
|
||||
**Additional placeholders**:
|
||||
- `${HOOKS}` - Hook declarations
|
||||
- `${HANDLERS}` - Event handler functions
|
||||
|
||||
### component-container-template.tsx
|
||||
|
||||
Container component template with data fetching.
|
||||
|
||||
**Additional placeholders**:
|
||||
- `${API_IMPORT}` - API function import
|
||||
- `${DATA_TYPE}` - Data type definition
|
||||
- `${FETCH_LOGIC}` - Data fetching implementation
|
||||
|
||||
### test-template.test.tsx
|
||||
|
||||
React Testing Library test template.
|
||||
|
||||
**Placeholders**:
|
||||
- `${COMPONENT_NAME}` - Component name
|
||||
- `${IMPORT_PATH}` - Import path
|
||||
- `${TEST_CASES}` - Generated test cases
|
||||
|
||||
### style-template.module.css
|
||||
|
||||
CSS Modules template.
|
||||
|
||||
**Placeholders**:
|
||||
- `${COMPONENT_NAME_KEBAB}` - Component name in kebab-case
|
||||
- `${BASE_STYLES}` - Base container styles
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
See `examples/` directory for reference implementations:
|
||||
|
||||
1. **Button.tsx** - Simple component with variants
|
||||
2. **SearchBar.tsx** - Component with hooks (useState, useEffect)
|
||||
3. **UserProfile.tsx** - Container component with data fetching
|
||||
|
||||
Each example includes:
|
||||
- Component implementation
|
||||
- Test file
|
||||
- Style file
|
||||
- Usage documentation
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Component Design
|
||||
- Keep components **small and focused** (single responsibility)
|
||||
- **Compose** complex UIs from simple components
|
||||
- **Lift state up** only when necessary
|
||||
- Use **descriptive names** (UserProfile, not UP)
|
||||
|
||||
### TypeScript
|
||||
- **Define prop interfaces** explicitly
|
||||
- **Avoid `any`** type (use `unknown` if needed)
|
||||
- **Export types** for consumers
|
||||
- **Use strict mode**
|
||||
|
||||
### Testing
|
||||
- **Test user behavior**, not implementation
|
||||
- **Query by role/text**, not test IDs
|
||||
- **Test accessible attributes**
|
||||
- **Mock external dependencies**
|
||||
|
||||
### Styling
|
||||
- **CSS Modules** for scoped styles
|
||||
- **BEM or descriptive class names**
|
||||
- **Mobile-first** responsive design
|
||||
- **Use CSS custom properties** for theming
|
||||
|
||||
### Accessibility
|
||||
- **Semantic HTML** (button, nav, main, etc.)
|
||||
- **ARIA labels** when needed
|
||||
- **Keyboard navigation** support
|
||||
- **Focus management** in modals/dropdowns
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Component Not Rendering
|
||||
|
||||
**Problem**: Generated component throws errors
|
||||
|
||||
**Solutions**:
|
||||
1. Check TypeScript compilation errors
|
||||
2. Verify all imports are correct
|
||||
3. Check props interface matches usage
|
||||
4. Validate JSX syntax
|
||||
|
||||
### Tests Failing
|
||||
|
||||
**Problem**: Generated tests don't pass
|
||||
|
||||
**Solutions**:
|
||||
1. Ensure React Testing Library is installed
|
||||
2. Check test queries match component output
|
||||
3. Verify mocks are set up correctly
|
||||
4. Run tests with `--verbose` flag
|
||||
|
||||
### Styles Not Applying
|
||||
|
||||
**Problem**: CSS modules not loading
|
||||
|
||||
**Solutions**:
|
||||
1. Check CSS module import syntax
|
||||
2. Verify webpack/vite config supports CSS modules
|
||||
3. Check className is applied to element
|
||||
4. Inspect browser devtools for loaded styles
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
**This skill succeeds when**:
|
||||
- [ ] Component file generated with valid TypeScript
|
||||
- [ ] Test file created with passing tests
|
||||
- [ ] Style file generated with scoped styles
|
||||
- [ ] Barrel export allows clean imports
|
||||
- [ ] Props interface matches requirements
|
||||
- [ ] Code follows React best practices
|
||||
- [ ] Accessibility attributes included
|
||||
|
||||
---
|
||||
|
||||
**Auto-invoke this skill when creating React components to ensure consistency and save time** ⚛️
|
||||
40
skills/frontend-component/examples/Button.tsx
Normal file
40
skills/frontend-component/examples/Button.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Button - Simple button component with variants
|
||||
*
|
||||
* @example
|
||||
* <Button variant="primary" onClick={() => console.log('clicked')}>
|
||||
* Click me
|
||||
* </Button>
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styles from './Button.module.css';
|
||||
|
||||
interface ButtonProps {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
variant?: 'primary' | 'secondary' | 'danger';
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
children,
|
||||
onClick,
|
||||
variant = 'primary',
|
||||
disabled = false,
|
||||
type = 'button',
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
className={`${styles.button} ${styles[variant]} ${className || ''}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
52
skills/frontend-component/examples/SearchBar.tsx
Normal file
52
skills/frontend-component/examples/SearchBar.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* SearchBar - Search input with debounced onChange
|
||||
*
|
||||
* @example
|
||||
* <SearchBar
|
||||
* onSearch={(query) => console.log('Search:', query)}
|
||||
* placeholder="Search users..."
|
||||
* />
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import styles from './SearchBar.module.css';
|
||||
|
||||
interface SearchBarProps {
|
||||
onSearch: (query: string) => void;
|
||||
placeholder?: string;
|
||||
debounceMs?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
onSearch,
|
||||
placeholder = 'Search...',
|
||||
debounceMs = 300,
|
||||
className,
|
||||
}) => {
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
if (query.trim()) {
|
||||
onSearch(query);
|
||||
}
|
||||
}, [query, onSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(handleSearch, debounceMs);
|
||||
return () => clearTimeout(timer);
|
||||
}, [query, debounceMs, handleSearch]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${className || ''}`}>
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={styles.input}
|
||||
aria-label="Search"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
89
skills/frontend-component/functions/component_generator.py
Executable file
89
skills/frontend-component/functions/component_generator.py
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate React component file from template with substitutions.
|
||||
|
||||
Replaces placeholders in template with component-specific values.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
|
||||
def read_template(template_path: str) -> str:
|
||||
"""Read template file content."""
|
||||
try:
|
||||
with open(template_path, 'r') as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"Template file not found: {template_path}")
|
||||
|
||||
def generate_component(name: str, props_interface: str, template_content: str, description: str = None) -> str:
|
||||
"""
|
||||
Generate component code by substituting placeholders in template.
|
||||
|
||||
Args:
|
||||
name: Component name (PascalCase)
|
||||
props_interface: Props interface name
|
||||
template_content: Template file content
|
||||
description: Brief component description
|
||||
|
||||
Returns:
|
||||
str: Generated component code
|
||||
"""
|
||||
# Convert PascalCase to kebab-case for file names
|
||||
kebab_name = ''.join(['-' + c.lower() if c.isupper() else c for c in name]).lstrip('-')
|
||||
|
||||
# Perform substitutions
|
||||
substitutions = {
|
||||
'${COMPONENT_NAME}': name,
|
||||
'${PROPS_INTERFACE}': props_interface,
|
||||
'${STYLE_IMPORT}': f"import styles from './{name}.module.css';",
|
||||
'${DESCRIPTION}': description or f"{name} component",
|
||||
'${COMPONENT_NAME_KEBAB}': kebab_name,
|
||||
}
|
||||
|
||||
result = template_content
|
||||
for placeholder, value in substitutions.items():
|
||||
result = result.replace(placeholder, value)
|
||||
|
||||
return result
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate React component from template')
|
||||
parser.add_argument('--name', required=True, help='Component name (PascalCase)')
|
||||
parser.add_argument('--type', default='simple', choices=['simple', 'with-hooks', 'container'], help='Component type')
|
||||
parser.add_argument('--props-interface', required=True, help='Props interface name')
|
||||
parser.add_argument('--template', required=True, help='Template file path')
|
||||
parser.add_argument('--output', help='Output file path (optional, prints to stdout if not provided)')
|
||||
parser.add_argument('--description', help='Component description')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# Read template
|
||||
template_content = read_template(args.template)
|
||||
|
||||
# Generate component
|
||||
component_code = generate_component(
|
||||
args.name,
|
||||
args.props_interface,
|
||||
template_content,
|
||||
args.description
|
||||
)
|
||||
|
||||
# Output
|
||||
if args.output:
|
||||
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
||||
with open(args.output, 'w') as f:
|
||||
f.write(component_code)
|
||||
print(f"✅ Component generated: {args.output}")
|
||||
else:
|
||||
print(component_code)
|
||||
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
115
skills/frontend-component/functions/name_validator.py
Executable file
115
skills/frontend-component/functions/name_validator.py
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validate component naming conventions.
|
||||
|
||||
Ensures component names follow PascalCase, are descriptive, and avoid reserved words.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
|
||||
# Reserved component names that should be avoided
|
||||
RESERVED_WORDS = {
|
||||
'Component', 'Element', 'Node', 'React', 'ReactNode', 'Fragment',
|
||||
'Props', 'State', 'Context', 'Provider', 'Consumer', 'Children',
|
||||
'Ref', 'Key', 'Type', 'Class', 'Function', 'Object', 'Array',
|
||||
'String', 'Number', 'Boolean', 'Symbol', 'Null', 'Undefined'
|
||||
}
|
||||
|
||||
def is_pascal_case(name):
|
||||
"""
|
||||
Check if name is in PascalCase format.
|
||||
|
||||
Args:
|
||||
name: String to validate
|
||||
|
||||
Returns:
|
||||
bool: True if PascalCase, False otherwise
|
||||
"""
|
||||
# PascalCase: starts with uppercase, contains only alphanumeric
|
||||
pattern = r'^[A-Z][a-zA-Z0-9]*$'
|
||||
return bool(re.match(pattern, name))
|
||||
|
||||
def validate_component_name(name):
|
||||
"""
|
||||
Validate component name against conventions.
|
||||
|
||||
Args:
|
||||
name: Component name to validate
|
||||
|
||||
Returns:
|
||||
tuple: (is_valid: bool, error_message: str or None)
|
||||
"""
|
||||
# Check length
|
||||
if len(name) < 2:
|
||||
return False, "Component name must be at least 2 characters long"
|
||||
|
||||
# Check for special characters
|
||||
if not name.replace('_', '').isalnum():
|
||||
return False, "Component name should only contain alphanumeric characters"
|
||||
|
||||
# Check PascalCase
|
||||
if not is_pascal_case(name):
|
||||
return False, f"Component name '{name}' must be in PascalCase (e.g., UserProfile, TodoList)"
|
||||
|
||||
# Check reserved words
|
||||
if name in RESERVED_WORDS:
|
||||
return False, f"'{name}' is a reserved word. Choose a more descriptive name."
|
||||
|
||||
# Check descriptiveness (not too generic)
|
||||
if len(name) < 4:
|
||||
return False, f"Component name '{name}' is too short. Use a more descriptive name (e.g., UserCard, not UC)"
|
||||
|
||||
# Check doesn't start with common anti-patterns
|
||||
anti_patterns = ['My', 'The', 'New', 'Test']
|
||||
if any(name.startswith(pattern) for pattern in anti_patterns):
|
||||
return False, f"Avoid starting component names with '{name[:3]}...'. Be more specific about what it does."
|
||||
|
||||
return True, None
|
||||
|
||||
def suggest_valid_name(name):
|
||||
"""
|
||||
Suggest a valid component name if the provided one is invalid.
|
||||
|
||||
Args:
|
||||
name: Invalid component name
|
||||
|
||||
Returns:
|
||||
str: Suggested valid name
|
||||
"""
|
||||
# Convert to PascalCase
|
||||
suggested = ''.join(word.capitalize() for word in re.split(r'[-_\s]+', name))
|
||||
|
||||
# Remove special characters
|
||||
suggested = re.sub(r'[^a-zA-Z0-9]', '', suggested)
|
||||
|
||||
# Ensure starts with uppercase
|
||||
if suggested and not suggested[0].isupper():
|
||||
suggested = suggested.capitalize()
|
||||
|
||||
return suggested if suggested else "MyComponent"
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Validate React component naming conventions')
|
||||
parser.add_argument('--name', required=True, help='Component name to validate')
|
||||
parser.add_argument('--suggest', action='store_true', help='Suggest a valid name if invalid')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
is_valid, error = validate_component_name(args.name)
|
||||
|
||||
if is_valid:
|
||||
print(f"✅ '{args.name}' is a valid component name")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"❌ Invalid component name: {error}", file=sys.stderr)
|
||||
|
||||
if args.suggest:
|
||||
suggested = suggest_valid_name(args.name)
|
||||
print(f"💡 Suggested name: {suggested}", file=sys.stderr)
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
125
skills/frontend-component/functions/props_interface_generator.py
Executable file
125
skills/frontend-component/functions/props_interface_generator.py
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate TypeScript props interface from user input.
|
||||
|
||||
Converts simple prop specifications into proper TypeScript interface definitions.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from typing import List, Tuple
|
||||
|
||||
# Type mapping from simple names to TypeScript types
|
||||
TYPE_MAPPING = {
|
||||
'string': 'string',
|
||||
'str': 'string',
|
||||
'number': 'number',
|
||||
'num': 'number',
|
||||
'int': 'number',
|
||||
'boolean': 'boolean',
|
||||
'bool': 'boolean',
|
||||
'function': '() => void',
|
||||
'func': '() => void',
|
||||
'callback': '() => void',
|
||||
'array': 'any[]',
|
||||
'arr': 'any[]',
|
||||
'object': 'Record<string, any>',
|
||||
'obj': 'Record<string, any>',
|
||||
'react-node': 'React.ReactNode',
|
||||
'node': 'React.ReactNode',
|
||||
'children': 'React.ReactNode',
|
||||
'element': 'React.ReactElement',
|
||||
'style': 'React.CSSProperties',
|
||||
'class': 'string',
|
||||
'classname': 'string',
|
||||
}
|
||||
|
||||
def parse_prop_spec(prop_spec: str) -> Tuple[str, str, bool]:
|
||||
"""
|
||||
Parse a single prop specification.
|
||||
|
||||
Format: "propName:type" or "propName:type:optional"
|
||||
|
||||
Args:
|
||||
prop_spec: Prop specification string
|
||||
|
||||
Returns:
|
||||
tuple: (prop_name, ts_type, is_optional)
|
||||
"""
|
||||
parts = prop_spec.strip().split(':')
|
||||
|
||||
if len(parts) < 2:
|
||||
raise ValueError(f"Invalid prop specification: '{prop_spec}'. Expected format: 'propName:type' or 'propName:type:optional'")
|
||||
|
||||
prop_name = parts[0].strip()
|
||||
type_name = parts[1].strip().lower()
|
||||
is_optional = len(parts) > 2 and parts[2].strip().lower() in ('optional', 'opt', '?', 'true')
|
||||
|
||||
# Map to TypeScript type
|
||||
ts_type = TYPE_MAPPING.get(type_name, type_name)
|
||||
|
||||
return prop_name, ts_type, is_optional
|
||||
|
||||
def generate_props_interface(name: str, props: List[str], include_common: bool = True) -> str:
|
||||
"""
|
||||
Generate TypeScript props interface.
|
||||
|
||||
Args:
|
||||
name: Component name (will become {name}Props)
|
||||
props: List of prop specifications
|
||||
include_common: Whether to include common props (children, className, etc.)
|
||||
|
||||
Returns:
|
||||
str: TypeScript interface definition
|
||||
"""
|
||||
interface_name = f"{name}Props"
|
||||
lines = [f"interface {interface_name} {{"]
|
||||
|
||||
# Add custom props
|
||||
for prop_spec in props:
|
||||
if not prop_spec.strip():
|
||||
continue
|
||||
|
||||
prop_name, ts_type, is_optional = parse_prop_spec(prop_spec)
|
||||
optional_marker = '?' if is_optional else ''
|
||||
lines.append(f" {prop_name}{optional_marker}: {ts_type};")
|
||||
|
||||
# Add common props if requested
|
||||
if include_common:
|
||||
# Only add children if not already specified
|
||||
if not any('children' in prop for prop in props):
|
||||
lines.append(" children?: React.ReactNode;")
|
||||
|
||||
# Only add className if not already specified
|
||||
if not any('className' in prop or 'class' in prop.lower() for prop in props):
|
||||
lines.append(" className?: string;")
|
||||
|
||||
lines.append("}")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate TypeScript props interface')
|
||||
parser.add_argument('--name', required=True, help='Component name')
|
||||
parser.add_argument('--props', required=True, help='Comma-separated prop specifications (e.g., "userId:string,onUpdate:function,isActive:boolean:optional")')
|
||||
parser.add_argument('--no-common', action='store_true', help='Do not include common props (children, className)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse prop specifications
|
||||
prop_specs = [p.strip() for p in args.props.split(',') if p.strip()]
|
||||
|
||||
try:
|
||||
interface = generate_props_interface(
|
||||
args.name,
|
||||
prop_specs,
|
||||
include_common=not args.no_common
|
||||
)
|
||||
print(interface)
|
||||
sys.exit(0)
|
||||
except ValueError as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
85
skills/frontend-component/functions/style_generator.py
Executable file
85
skills/frontend-component/functions/style_generator.py
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate style file (CSS Modules or Styled Components).
|
||||
|
||||
Creates scoped styles for React components.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
|
||||
def read_template(template_path: str) -> str:
|
||||
"""Read template file content."""
|
||||
try:
|
||||
with open(template_path, 'r') as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"Template file not found: {template_path}")
|
||||
|
||||
def generate_style(name: str, approach: str, template_content: str) -> str:
|
||||
"""
|
||||
Generate style code by substituting placeholders in template.
|
||||
|
||||
Args:
|
||||
name: Component name (PascalCase)
|
||||
approach: Styling approach (css-modules, styled-components, tailwind)
|
||||
template_content: Template file content
|
||||
|
||||
Returns:
|
||||
str: Generated style code
|
||||
"""
|
||||
# Convert PascalCase to kebab-case
|
||||
kebab_name = ''.join(['-' + c.lower() if c.isupper() else c for c in name]).lstrip('-')
|
||||
|
||||
# Perform substitutions
|
||||
substitutions = {
|
||||
'${COMPONENT_NAME}': name,
|
||||
'${COMPONENT_NAME_KEBAB}': kebab_name,
|
||||
'${BASE_STYLES}': """ display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;""",
|
||||
}
|
||||
|
||||
result = template_content
|
||||
for placeholder, value in substitutions.items():
|
||||
result = result.replace(placeholder, value)
|
||||
|
||||
return result
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate React component style file')
|
||||
parser.add_argument('--name', required=True, help='Component name (PascalCase)')
|
||||
parser.add_argument('--approach', default='css-modules', choices=['css-modules', 'styled-components', 'tailwind'], help='Styling approach')
|
||||
parser.add_argument('--template', required=True, help='Style template file path')
|
||||
parser.add_argument('--output', help='Output file path (optional, prints to stdout if not provided)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# Read template
|
||||
template_content = read_template(args.template)
|
||||
|
||||
# Generate style
|
||||
style_code = generate_style(
|
||||
args.name,
|
||||
args.approach,
|
||||
template_content
|
||||
)
|
||||
|
||||
# Output
|
||||
if args.output:
|
||||
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
||||
with open(args.output, 'w') as f:
|
||||
f.write(style_code)
|
||||
print(f"✅ Style file generated: {args.output}")
|
||||
else:
|
||||
print(style_code)
|
||||
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
100
skills/frontend-component/functions/test_generator.py
Executable file
100
skills/frontend-component/functions/test_generator.py
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate test file with React Testing Library.
|
||||
|
||||
Creates comprehensive test suite for React components.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
|
||||
def read_template(template_path: str) -> str:
|
||||
"""Read template file content."""
|
||||
try:
|
||||
with open(template_path, 'r') as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"Template file not found: {template_path}")
|
||||
|
||||
def generate_test(component_name: str, component_path: str, template_content: str) -> str:
|
||||
"""
|
||||
Generate test code by substituting placeholders in template.
|
||||
|
||||
Args:
|
||||
component_name: Component name (PascalCase)
|
||||
component_path: Path to component file
|
||||
template_content: Template file content
|
||||
|
||||
Returns:
|
||||
str: Generated test code
|
||||
"""
|
||||
# Calculate relative import path
|
||||
import_path = f'./{component_name}'
|
||||
|
||||
# Basic test cases (can be expanded based on props analysis)
|
||||
test_cases = f"""
|
||||
it('renders without crashing', () => {{
|
||||
render(<{component_name} />);
|
||||
}});
|
||||
|
||||
it('renders children correctly', () => {{
|
||||
render(<{component_name}>Test Content</{component_name}>);
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
}});
|
||||
|
||||
it('applies custom className', () => {{
|
||||
const {{ container }} = render(<{component_name} className="custom-class" />);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
}});
|
||||
""".strip()
|
||||
|
||||
# Perform substitutions
|
||||
substitutions = {
|
||||
'${COMPONENT_NAME}': component_name,
|
||||
'${IMPORT_PATH}': import_path,
|
||||
'${TEST_CASES}': test_cases,
|
||||
}
|
||||
|
||||
result = template_content
|
||||
for placeholder, value in substitutions.items():
|
||||
result = result.replace(placeholder, value)
|
||||
|
||||
return result
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate React component test file')
|
||||
parser.add_argument('--component-name', required=True, help='Component name (PascalCase)')
|
||||
parser.add_argument('--component-path', required=True, help='Path to component file')
|
||||
parser.add_argument('--template', required=True, help='Test template file path')
|
||||
parser.add_argument('--output', help='Output file path (optional, prints to stdout if not provided)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# Read template
|
||||
template_content = read_template(args.template)
|
||||
|
||||
# Generate test
|
||||
test_code = generate_test(
|
||||
args.component_name,
|
||||
args.component_path,
|
||||
template_content
|
||||
)
|
||||
|
||||
# Output
|
||||
if args.output:
|
||||
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
||||
with open(args.output, 'w') as f:
|
||||
f.write(test_code)
|
||||
print(f"✅ Test file generated: {args.output}")
|
||||
else:
|
||||
print(test_code)
|
||||
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* ${COMPONENT_NAME} - ${DESCRIPTION}
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
${STYLE_IMPORT}
|
||||
|
||||
${PROPS_INTERFACE}
|
||||
|
||||
export const ${COMPONENT_NAME}: React.FC<${PROPS_INTERFACE}> = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${styles.container} ${className || ''}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
/* ${COMPONENT_NAME} Styles */
|
||||
|
||||
.container {
|
||||
${BASE_STYLES}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ${COMPONENT_NAME} } from '${IMPORT_PATH}';
|
||||
|
||||
describe('${COMPONENT_NAME}', () => {
|
||||
${TEST_CASES}
|
||||
});
|
||||
Reference in New Issue
Block a user