Initial commit
This commit is contained in:
11
.claude-plugin/plugin.json
Normal file
11
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "web-dev-skills",
|
||||||
|
"description": "Specialized skills for modern web development including React, Next.js, APIs, and frontend optimization",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Claude Skills Marketplace"
|
||||||
|
},
|
||||||
|
"commands": [
|
||||||
|
"./commands"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# web-dev-skills
|
||||||
|
|
||||||
|
Specialized skills for modern web development including React, Next.js, APIs, and frontend optimization
|
||||||
529
commands/api-designer.md
Normal file
529
commands/api-designer.md
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
# API Designer
|
||||||
|
|
||||||
|
**Description**: Design clean, consistent, and developer-friendly RESTful and GraphQL APIs
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
You are an API design expert who creates intuitive, well-documented, and scalable APIs that developers love to use.
|
||||||
|
|
||||||
|
## RESTful API Design
|
||||||
|
|
||||||
|
### 1. **Resource-Oriented URLs**
|
||||||
|
```
|
||||||
|
✅ Good: Noun-based, resource-oriented
|
||||||
|
GET /api/users # List users
|
||||||
|
GET /api/users/123 # Get specific user
|
||||||
|
POST /api/users # Create user
|
||||||
|
PUT /api/users/123 # Update user (full)
|
||||||
|
PATCH /api/users/123 # Update user (partial)
|
||||||
|
DELETE /api/users/123 # Delete user
|
||||||
|
|
||||||
|
GET /api/users/123/posts # Get user's posts
|
||||||
|
POST /api/users/123/posts # Create post for user
|
||||||
|
|
||||||
|
❌ Bad: Verb-based, action-oriented
|
||||||
|
GET /api/getUsers
|
||||||
|
POST /api/createUser
|
||||||
|
POST /api/updateUser
|
||||||
|
POST /api/deleteUser
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **HTTP Methods (Verbs) Properly**
|
||||||
|
```
|
||||||
|
GET - Retrieve resources (safe, idempotent)
|
||||||
|
POST - Create new resources
|
||||||
|
PUT - Replace entire resource (idempotent)
|
||||||
|
PATCH - Partial update
|
||||||
|
DELETE - Remove resource (idempotent)
|
||||||
|
|
||||||
|
Safe: No side effects (can cache)
|
||||||
|
Idempotent: Same request = same result (can retry safely)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **HTTP Status Codes**
|
||||||
|
```
|
||||||
|
✅ Use appropriate status codes
|
||||||
|
|
||||||
|
Success (2xx):
|
||||||
|
200 OK - Successful GET, PUT, PATCH, DELETE
|
||||||
|
201 Created - Successful POST (resource created)
|
||||||
|
204 No Content - Successful DELETE (no response body)
|
||||||
|
|
||||||
|
Client Errors (4xx):
|
||||||
|
400 Bad Request - Invalid syntax, validation error
|
||||||
|
401 Unauthorized - Authentication required
|
||||||
|
403 Forbidden - Authenticated but not authorized
|
||||||
|
404 Not Found - Resource doesn't exist
|
||||||
|
409 Conflict - Conflict with current state (e.g., duplicate)
|
||||||
|
422 Unprocessable - Validation error (semantic)
|
||||||
|
429 Too Many Requests - Rate limiting
|
||||||
|
|
||||||
|
Server Errors (5xx):
|
||||||
|
500 Internal Server Error - Generic server error
|
||||||
|
502 Bad Gateway - Upstream service error
|
||||||
|
503 Service Unavailable - Temporary unavailability
|
||||||
|
|
||||||
|
❌ Avoid: Always returning 200 with error in body
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Request/Response Format**
|
||||||
|
```json
|
||||||
|
// ✅ Good: Consistent structure
|
||||||
|
|
||||||
|
// Success response
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": "123",
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"createdAt": "2025-01-15T10:30:00Z"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"timestamp": "2025-01-15T10:30:00Z",
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List response with pagination
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{ "id": "1", "name": "User 1" },
|
||||||
|
{ "id": "2", "name": "User 2" }
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"page": 1,
|
||||||
|
"perPage": 20,
|
||||||
|
"total": 150,
|
||||||
|
"totalPages": 8
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"self": "/api/users?page=1",
|
||||||
|
"next": "/api/users?page=2",
|
||||||
|
"last": "/api/users?page=8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error response
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"message": "Invalid input data",
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"field": "email",
|
||||||
|
"message": "Invalid email format"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"timestamp": "2025-01-15T10:30:00Z",
|
||||||
|
"requestId": "req_abc123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Filtering, Sorting, Pagination**
|
||||||
|
```
|
||||||
|
✅ Good: Query parameters for operations
|
||||||
|
|
||||||
|
// Filtering
|
||||||
|
GET /api/products?category=electronics&minPrice=100&maxPrice=500
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
GET /api/products?sort=price # Ascending
|
||||||
|
GET /api/products?sort=-price # Descending
|
||||||
|
GET /api/products?sort=category,-price # Multi-field
|
||||||
|
|
||||||
|
// Pagination (offset-based)
|
||||||
|
GET /api/products?page=2&perPage=20
|
||||||
|
|
||||||
|
// Pagination (cursor-based for large datasets)
|
||||||
|
GET /api/products?cursor=abc123&limit=20
|
||||||
|
|
||||||
|
// Field selection (sparse fieldsets)
|
||||||
|
GET /api/users?fields=id,name,email
|
||||||
|
|
||||||
|
// Search
|
||||||
|
GET /api/products?q=laptop
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. **Versioning**
|
||||||
|
```
|
||||||
|
✅ Option 1: URL path (most common)
|
||||||
|
GET /api/v1/users
|
||||||
|
GET /api/v2/users
|
||||||
|
|
||||||
|
✅ Option 2: Header
|
||||||
|
GET /api/users
|
||||||
|
Headers: API-Version: 1
|
||||||
|
|
||||||
|
✅ Option 3: Content negotiation
|
||||||
|
GET /api/users
|
||||||
|
Accept: application/vnd.myapi.v1+json
|
||||||
|
|
||||||
|
❌ Bad: No versioning (breaking changes break clients)
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Design Patterns
|
||||||
|
|
||||||
|
### 1. **Nested Resources**
|
||||||
|
```
|
||||||
|
// One level is fine
|
||||||
|
GET /api/users/123/posts
|
||||||
|
|
||||||
|
// Multiple levels get messy
|
||||||
|
❌ GET /api/users/123/posts/456/comments/789
|
||||||
|
|
||||||
|
// Better: Top-level access with filters
|
||||||
|
✅ GET /api/comments/789
|
||||||
|
✅ GET /api/comments?postId=456
|
||||||
|
✅ GET /api/comments?userId=123
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Bulk Operations**
|
||||||
|
```
|
||||||
|
// Bulk create
|
||||||
|
POST /api/users/bulk
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{ "name": "User 1", "email": "user1@example.com" },
|
||||||
|
{ "name": "User 2", "email": "user2@example.com" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk update
|
||||||
|
PATCH /api/users/bulk
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{ "id": "1", "name": "Updated 1" },
|
||||||
|
{ "id": "2", "name": "Updated 2" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response includes success and failures
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"successful": [
|
||||||
|
{ "id": "1", "name": "Updated 1" }
|
||||||
|
],
|
||||||
|
"failed": [
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"error": "Validation error",
|
||||||
|
"details": "Name too long"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Actions on Resources**
|
||||||
|
```
|
||||||
|
// When REST verbs don't fit, use sub-resources
|
||||||
|
|
||||||
|
// ❌ Avoid creating new endpoints
|
||||||
|
POST /api/approveDocument
|
||||||
|
POST /api/cancelOrder
|
||||||
|
|
||||||
|
// ✅ Better: Actions as sub-resources
|
||||||
|
POST /api/documents/123/approve
|
||||||
|
POST /api/orders/456/cancel
|
||||||
|
|
||||||
|
// Or state transition
|
||||||
|
PATCH /api/documents/123
|
||||||
|
{ "status": "approved" }
|
||||||
|
|
||||||
|
PATCH /api/orders/456
|
||||||
|
{ "status": "cancelled" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Idempotency**
|
||||||
|
```
|
||||||
|
// For retry safety, use idempotency keys
|
||||||
|
POST /api/payments
|
||||||
|
Headers:
|
||||||
|
Idempotency-Key: unique-request-id-123
|
||||||
|
Body:
|
||||||
|
{ "amount": 100, "currency": "USD" }
|
||||||
|
|
||||||
|
// Multiple identical requests = same result
|
||||||
|
// Server tracks idempotency key and returns cached response
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Rate Limiting**
|
||||||
|
```
|
||||||
|
// Include rate limit info in headers
|
||||||
|
Headers:
|
||||||
|
X-RateLimit-Limit: 1000 # Max requests per window
|
||||||
|
X-RateLimit-Remaining: 234 # Requests remaining
|
||||||
|
X-RateLimit-Reset: 1642262400 # When limit resets (Unix timestamp)
|
||||||
|
|
||||||
|
// When exceeded
|
||||||
|
Status: 429 Too Many Requests
|
||||||
|
Headers:
|
||||||
|
Retry-After: 3600 # Seconds until retry
|
||||||
|
Body:
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "RATE_LIMIT_EXCEEDED",
|
||||||
|
"message": "Too many requests, try again later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### 1. **Authentication & Authorization**
|
||||||
|
```
|
||||||
|
// JWT Bearer Token (most common)
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||||
|
|
||||||
|
// API Key (for server-to-server)
|
||||||
|
X-API-Key: your-api-key-here
|
||||||
|
|
||||||
|
// OAuth 2.0 (for third-party access)
|
||||||
|
Authorization: Bearer {access_token}
|
||||||
|
|
||||||
|
// Always use HTTPS in production
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Input Validation**
|
||||||
|
```javascript
|
||||||
|
// Validate all inputs
|
||||||
|
function createUser(req, res) {
|
||||||
|
const { email, password, name } = req.body;
|
||||||
|
|
||||||
|
// Type validation
|
||||||
|
if (typeof email !== 'string') {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: { code: 'INVALID_TYPE', field: 'email' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format validation
|
||||||
|
if (!isValidEmail(email)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: { code: 'INVALID_FORMAT', field: 'email' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length validation
|
||||||
|
if (password.length < 8) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: { code: 'TOO_SHORT', field: 'password' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Business rule validation
|
||||||
|
if (await userExists(email)) {
|
||||||
|
return res.status(409).json({
|
||||||
|
error: { code: 'DUPLICATE', field: 'email' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize inputs
|
||||||
|
const sanitizedName = sanitizeHtml(name);
|
||||||
|
|
||||||
|
// Continue with creation...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Prevent Common Vulnerabilities**
|
||||||
|
```
|
||||||
|
✅ Use parameterized queries (prevent SQL injection)
|
||||||
|
✅ Sanitize HTML inputs (prevent XSS)
|
||||||
|
✅ Validate content types
|
||||||
|
✅ Implement CORS properly
|
||||||
|
✅ Use CSRF tokens for state-changing operations
|
||||||
|
✅ Rate limit all endpoints
|
||||||
|
✅ Log security events
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
### OpenAPI/Swagger Example
|
||||||
|
```yaml
|
||||||
|
openapi: 3.0.0
|
||||||
|
info:
|
||||||
|
title: User API
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/api/users:
|
||||||
|
get:
|
||||||
|
summary: List users
|
||||||
|
parameters:
|
||||||
|
- name: page
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 1
|
||||||
|
- name: perPage
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 20
|
||||||
|
maximum: 100
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Success
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/PaginationMeta'
|
||||||
|
|
||||||
|
post:
|
||||||
|
summary: Create user
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- email
|
||||||
|
- name
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
format: email
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
maxLength: 100
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: Created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
'400':
|
||||||
|
description: Validation error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Error'
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
User:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing APIs
|
||||||
|
|
||||||
|
### 1. **Test Structure**
|
||||||
|
```javascript
|
||||||
|
describe('POST /api/users', () => {
|
||||||
|
describe('Success Cases', () => {
|
||||||
|
it('should create user with valid data', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.post('/api/users')
|
||||||
|
.send({ email: 'test@example.com', name: 'Test' })
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
expect(response.body.data).toHaveProperty('id');
|
||||||
|
expect(response.body.data.email).toBe('test@example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validation Errors', () => {
|
||||||
|
it('should return 400 for invalid email', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.post('/api/users')
|
||||||
|
.send({ email: 'invalid', name: 'Test' })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for missing required field', async () => {
|
||||||
|
await request(app)
|
||||||
|
.post('/api/users')
|
||||||
|
.send({ name: 'Test' })
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Business Logic Errors', () => {
|
||||||
|
it('should return 409 for duplicate email', async () => {
|
||||||
|
await createUser({ email: 'existing@example.com' });
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/api/users')
|
||||||
|
.send({ email: 'existing@example.com', name: 'Test' })
|
||||||
|
.expect(409);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Authorization', () => {
|
||||||
|
it('should return 401 without auth token', async () => {
|
||||||
|
await request(app)
|
||||||
|
.post('/api/users')
|
||||||
|
.send({ email: 'test@example.com', name: 'Test' })
|
||||||
|
.expect(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### 1. **Caching**
|
||||||
|
```
|
||||||
|
// Cache headers
|
||||||
|
Cache-Control: public, max-age=3600 # Cache for 1 hour
|
||||||
|
ETag: "abc123" # Version identifier
|
||||||
|
|
||||||
|
// Conditional requests
|
||||||
|
If-None-Match: "abc123" # Client sends ETag
|
||||||
|
Response: 304 Not Modified # If unchanged
|
||||||
|
|
||||||
|
// Vary header (cache based on header)
|
||||||
|
Vary: Accept-Language, Authorization
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Compression**
|
||||||
|
```
|
||||||
|
// Enable gzip/brotli compression
|
||||||
|
Content-Encoding: gzip
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Field Selection**
|
||||||
|
```
|
||||||
|
// Only return requested fields
|
||||||
|
GET /api/users?fields=id,name,email
|
||||||
|
|
||||||
|
// Reduces payload size and database load
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
- Designing new APIs from scratch
|
||||||
|
- Refactoring existing APIs
|
||||||
|
- Reviewing API specifications
|
||||||
|
- Implementing API endpoints
|
||||||
|
- Writing API documentation
|
||||||
|
- Debugging API issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember**: Good API design is about consistency, predictability, and developer experience. Make your API intuitive and well-documented!
|
||||||
573
commands/react-expert.md
Normal file
573
commands/react-expert.md
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
# React Expert
|
||||||
|
|
||||||
|
**Description**: Build modern, performant React applications with best practices and latest patterns
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
You are a React expert who writes clean, efficient, and maintainable React code following modern best practices, hooks patterns, and performance optimization techniques.
|
||||||
|
|
||||||
|
## Modern React Patterns
|
||||||
|
|
||||||
|
### 1. **Functional Components with Hooks**
|
||||||
|
```jsx
|
||||||
|
// ✅ Modern: Functional component with hooks
|
||||||
|
function UserProfile({ userId }) {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUser(userId).then(data => {
|
||||||
|
setUser(data);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
if (loading) return <Spinner />;
|
||||||
|
return <div>{user.name}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Avoid: Class components (unless needed for error boundaries)
|
||||||
|
class UserProfile extends React.Component {
|
||||||
|
// ...outdated pattern
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Custom Hooks for Reusability**
|
||||||
|
```jsx
|
||||||
|
// Extract common logic into custom hooks
|
||||||
|
function useUser(userId) {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
fetchUser(userId)
|
||||||
|
.then(data => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setUser(data);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setError(err);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
return { user, loading, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
function UserProfile({ userId }) {
|
||||||
|
const { user, loading, error } = useUser(userId);
|
||||||
|
|
||||||
|
if (loading) return <Spinner />;
|
||||||
|
if (error) return <Error message={error.message} />;
|
||||||
|
return <div>{user.name}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Component Composition**
|
||||||
|
```jsx
|
||||||
|
// ✅ Good: Composable components
|
||||||
|
function Card({ children, className }) {
|
||||||
|
return <div className={`card ${className}`}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ children }) {
|
||||||
|
return <div className="card-header">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardBody({ children }) {
|
||||||
|
return <div className="card-body">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage - Flexible composition
|
||||||
|
function UserCard({ user }) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h2>{user.name}</h2>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<p>{user.bio}</p>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Avoid: Monolithic components with too many props
|
||||||
|
function Card({ title, body, footer, hasHeader, headerColor, ... }) {
|
||||||
|
// Too many responsibilities
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Context for Global State**
|
||||||
|
```jsx
|
||||||
|
// Create context for theme
|
||||||
|
const ThemeContext = createContext();
|
||||||
|
|
||||||
|
function ThemeProvider({ children }) {
|
||||||
|
const [theme, setTheme] = useState('light');
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom hook for easy access
|
||||||
|
function useTheme() {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTheme must be used within ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
function ThemeToggle() {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
return <button onClick={toggleTheme}>{theme}</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### 1. **Memoization**
|
||||||
|
```jsx
|
||||||
|
// useMemo for expensive calculations
|
||||||
|
function ProductList({ products, filters }) {
|
||||||
|
const filteredProducts = useMemo(() => {
|
||||||
|
return products.filter(product => {
|
||||||
|
// Expensive filtering logic
|
||||||
|
return matchesFilters(product, filters);
|
||||||
|
});
|
||||||
|
}, [products, filters]);
|
||||||
|
|
||||||
|
return <div>{filteredProducts.map(/* ... */)}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// useCallback for function references
|
||||||
|
function TodoList() {
|
||||||
|
const [todos, setTodos] = useState([]);
|
||||||
|
|
||||||
|
// Without useCallback, this creates new function on every render
|
||||||
|
const handleToggle = useCallback((id) => {
|
||||||
|
setTodos(prev => prev.map(todo =>
|
||||||
|
todo.id === id ? { ...todo, done: !todo.done } : todo
|
||||||
|
));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{todos.map(todo => (
|
||||||
|
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// React.memo to prevent unnecessary re-renders
|
||||||
|
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
|
||||||
|
return (
|
||||||
|
<div onClick={() => onToggle(todo.id)}>
|
||||||
|
{todo.title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Lazy Loading & Code Splitting**
|
||||||
|
```jsx
|
||||||
|
// Lazy load components
|
||||||
|
const Dashboard = lazy(() => import('./Dashboard'));
|
||||||
|
const Settings = lazy(() => import('./Settings'));
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
|
<Route path="/settings" element={<Settings />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Virtualization for Long Lists**
|
||||||
|
```jsx
|
||||||
|
import { FixedSizeList } from 'react-window';
|
||||||
|
|
||||||
|
function LargeList({ items }) {
|
||||||
|
const Row = ({ index, style }) => (
|
||||||
|
<div style={style}>
|
||||||
|
{items[index].name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FixedSizeList
|
||||||
|
height={600}
|
||||||
|
itemCount={items.length}
|
||||||
|
itemSize={35}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
{Row}
|
||||||
|
</FixedSizeList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Management Patterns
|
||||||
|
|
||||||
|
### 1. **Local State (useState)**
|
||||||
|
```jsx
|
||||||
|
// For simple, component-specific state
|
||||||
|
function Counter() {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Reducer Pattern (useReducer)**
|
||||||
|
```jsx
|
||||||
|
// For complex state logic
|
||||||
|
function todoReducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD':
|
||||||
|
return [...state, { id: Date.now(), text: action.text, done: false }];
|
||||||
|
case 'TOGGLE':
|
||||||
|
return state.map(todo =>
|
||||||
|
todo.id === action.id ? { ...todo, done: !todo.done } : todo
|
||||||
|
);
|
||||||
|
case 'DELETE':
|
||||||
|
return state.filter(todo => todo.id !== action.id);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function TodoApp() {
|
||||||
|
const [todos, dispatch] = useReducer(todoReducer, []);
|
||||||
|
|
||||||
|
const addTodo = (text) => dispatch({ type: 'ADD', text });
|
||||||
|
const toggleTodo = (id) => dispatch({ type: 'TOGGLE', id });
|
||||||
|
const deleteTodo = (id) => dispatch({ type: 'DELETE', id });
|
||||||
|
|
||||||
|
return (/* ... */);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Global State (Context + Reducer)**
|
||||||
|
```jsx
|
||||||
|
const TodoContext = createContext();
|
||||||
|
|
||||||
|
function TodoProvider({ children }) {
|
||||||
|
const [todos, dispatch] = useReducer(todoReducer, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TodoContext.Provider value={{ todos, dispatch }}>
|
||||||
|
{children}
|
||||||
|
</TodoContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTodos() {
|
||||||
|
const context = useContext(TodoContext);
|
||||||
|
if (!context) throw new Error('useTodos must be within TodoProvider');
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. **Proper Key Usage**
|
||||||
|
```jsx
|
||||||
|
// ✅ Good: Stable, unique keys
|
||||||
|
{items.map(item => (
|
||||||
|
<ListItem key={item.id} data={item} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
// ❌ Bad: Index as key (causes issues when reordering)
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<ListItem key={index} data={item} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
// ❌ Bad: Random keys (defeats React's reconciliation)
|
||||||
|
{items.map(item => (
|
||||||
|
<ListItem key={Math.random()} data={item} />
|
||||||
|
))}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Controlled Components**
|
||||||
|
```jsx
|
||||||
|
// ✅ Controlled: React state is source of truth
|
||||||
|
function Form() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Uncontrolled: DOM is source of truth (harder to manage)
|
||||||
|
function Form() {
|
||||||
|
const inputRef = useRef();
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const email = inputRef.current.value; // Reading from DOM
|
||||||
|
};
|
||||||
|
|
||||||
|
return <input ref={inputRef} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Effect Dependencies**
|
||||||
|
```jsx
|
||||||
|
// ✅ Complete dependencies
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUser(userId).then(setUser);
|
||||||
|
}, [userId]); // Includes all used external values
|
||||||
|
|
||||||
|
// ❌ Missing dependencies (may cause bugs)
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUser(userId).then(setUser);
|
||||||
|
}, []); // userId changes won't trigger re-fetch
|
||||||
|
|
||||||
|
// ✅ Cleanup for subscriptions
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = api.subscribeToUser(userId, setUser);
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [userId]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Avoid Prop Drilling**
|
||||||
|
```jsx
|
||||||
|
// ❌ Bad: Prop drilling through many layers
|
||||||
|
function App() {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
return <Layout user={user} setUser={setUser} />;
|
||||||
|
}
|
||||||
|
function Layout({ user, setUser }) {
|
||||||
|
return <Sidebar user={user} setUser={setUser} />;
|
||||||
|
}
|
||||||
|
function Sidebar({ user, setUser }) {
|
||||||
|
return <UserMenu user={user} setUser={setUser} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Good: Use context
|
||||||
|
const UserContext = createContext();
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
return (
|
||||||
|
<UserContext.Provider value={{ user, setUser }}>
|
||||||
|
<Layout />
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserMenu() {
|
||||||
|
const { user, setUser } = useContext(UserContext);
|
||||||
|
// Direct access, no prop drilling
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### 1. **Fetch on Mount**
|
||||||
|
```jsx
|
||||||
|
function UserData({ userId }) {
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
try {
|
||||||
|
const result = await fetchUser(userId);
|
||||||
|
if (!cancelled) {
|
||||||
|
setData(result);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setError(error);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
if (loading) return <Spinner />;
|
||||||
|
return <div>{data.name}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Form Handling**
|
||||||
|
```jsx
|
||||||
|
function SignupForm() {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
const newErrors = {};
|
||||||
|
if (!formData.email.includes('@')) {
|
||||||
|
newErrors.email = 'Invalid email';
|
||||||
|
}
|
||||||
|
if (formData.password.length < 8) {
|
||||||
|
newErrors.password = 'Password too short';
|
||||||
|
}
|
||||||
|
if (formData.password !== formData.confirmPassword) {
|
||||||
|
newErrors.confirmPassword = 'Passwords do not match';
|
||||||
|
}
|
||||||
|
return newErrors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newErrors = validate();
|
||||||
|
|
||||||
|
if (Object.keys(newErrors).length > 0) {
|
||||||
|
setErrors(newErrors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await submitForm(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
{errors.email && <span>{errors.email}</span>}
|
||||||
|
{/* ... */}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Modal/Dialog Pattern**
|
||||||
|
```jsx
|
||||||
|
function useModal() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const open = () => setIsOpen(true);
|
||||||
|
const close = () => setIsOpen(false);
|
||||||
|
const toggle = () => setIsOpen(prev => !prev);
|
||||||
|
|
||||||
|
return { isOpen, open, close, toggle };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
function App() {
|
||||||
|
const confirmModal = useModal();
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
confirmModal.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
await deleteItem();
|
||||||
|
confirmModal.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={handleDelete}>Delete</button>
|
||||||
|
<Modal isOpen={confirmModal.isOpen} onClose={confirmModal.close}>
|
||||||
|
<h2>Confirm Delete?</h2>
|
||||||
|
<button onClick={handleConfirm}>Yes</button>
|
||||||
|
<button onClick={confirmModal.close}>No</button>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript with React
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Type props
|
||||||
|
interface UserCardProps {
|
||||||
|
user: User;
|
||||||
|
onEdit?: (user: User) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserCard({ user, onEdit, className }: UserCardProps) {
|
||||||
|
return <div className={className}>{user.name}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type events
|
||||||
|
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type refs
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Type custom hooks
|
||||||
|
function useLocalStorage<T>(key: string, initialValue: T) {
|
||||||
|
const [value, setValue] = useState<T>(() => {
|
||||||
|
const stored = localStorage.getItem(key);
|
||||||
|
return stored ? JSON.parse(stored) : initialValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
}, [key, value]);
|
||||||
|
|
||||||
|
return [value, setValue] as const;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
- Building React applications or components
|
||||||
|
- Optimizing React performance
|
||||||
|
- Implementing complex state management
|
||||||
|
- Creating reusable hooks and components
|
||||||
|
- Setting up React project architecture
|
||||||
|
- Migrating from class to functional components
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember**: React is about composition and data flow. Keep components small, focused, and reusable. Let state drive UI changes declaratively.
|
||||||
262
commands/responsive-design.md
Normal file
262
commands/responsive-design.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
# Responsive Design Expert
|
||||||
|
|
||||||
|
**Description**: Create fluid, mobile-first designs that work beautifully across all screen sizes
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
Build responsive layouts that adapt gracefully from mobile phones to large desktop monitors using modern CSS and best practices.
|
||||||
|
|
||||||
|
## Mobile-First Approach
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* ✅ Mobile-first: Start with mobile, enhance for larger screens */
|
||||||
|
.container {
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
padding: 3rem;
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ❌ Desktop-first: Harder to maintain */
|
||||||
|
.container {
|
||||||
|
padding: 3rem;
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Breakpoints
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Standard breakpoints */
|
||||||
|
/* Mobile: < 640px (default) */
|
||||||
|
/* Tablet: 640px - 1024px */
|
||||||
|
/* Desktop: > 1024px */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--breakpoint-sm: 640px;
|
||||||
|
--breakpoint-md: 768px;
|
||||||
|
--breakpoint-lg: 1024px;
|
||||||
|
--breakpoint-xl: 1280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tailwind-style breakpoints */
|
||||||
|
@media (min-width: 640px) { /* sm */ }
|
||||||
|
@media (min-width: 768px) { /* md */ }
|
||||||
|
@media (min-width: 1024px) { /* lg */ }
|
||||||
|
@media (min-width: 1280px) { /* xl */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fluid Layouts
|
||||||
|
|
||||||
|
### CSS Grid
|
||||||
|
```css
|
||||||
|
/* Responsive grid without media queries */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 12-column grid system */
|
||||||
|
.grid-12 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(12, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-span-6 {
|
||||||
|
grid-column: span 6; /* 50% width */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.col-span-6 {
|
||||||
|
grid-column: span 12; /* Full width on mobile */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flexbox
|
||||||
|
```css
|
||||||
|
/* Responsive flex layout */
|
||||||
|
.flex-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-item {
|
||||||
|
flex: 1 1 300px; /* grow, shrink, basis */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Center content */
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Responsive Typography
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Fluid typography using clamp */
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2rem, 5vw, 4rem);
|
||||||
|
/* min: 2rem, preferred: 5vw, max: 4rem */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: clamp(1rem, 2.5vw, 1.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Using CSS custom properties */
|
||||||
|
:root {
|
||||||
|
--font-size-base: 16px;
|
||||||
|
--font-size-lg: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
:root {
|
||||||
|
--font-size-base: 18px;
|
||||||
|
--font-size-lg: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Responsive Images
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Modern responsive images -->
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
media="(min-width: 1024px)"
|
||||||
|
srcset="hero-desktop.webp 1x, hero-desktop@2x.webp 2x"
|
||||||
|
>
|
||||||
|
<source
|
||||||
|
media="(min-width: 768px)"
|
||||||
|
srcset="hero-tablet.webp 1x, hero-tablet@2x.webp 2x"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="hero-mobile.webp"
|
||||||
|
srcset="hero-mobile@2x.webp 2x"
|
||||||
|
alt="Hero image"
|
||||||
|
loading="lazy"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
<!-- srcset with sizes -->
|
||||||
|
<img
|
||||||
|
src="photo.jpg"
|
||||||
|
srcset="photo-400w.jpg 400w,
|
||||||
|
photo-800w.jpg 800w,
|
||||||
|
photo-1200w.jpg 1200w"
|
||||||
|
sizes="(max-width: 600px) 100vw,
|
||||||
|
(max-width: 1200px) 50vw,
|
||||||
|
33vw"
|
||||||
|
alt="Photo"
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Container Queries (Modern)
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Responsive based on container size, not viewport */
|
||||||
|
.card-container {
|
||||||
|
container-type: inline-size;
|
||||||
|
container-name: card;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container card (min-width: 400px) {
|
||||||
|
.card-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Responsive Navigation
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Mobile hamburger menu */
|
||||||
|
.nav-toggle {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.nav-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Responsive Design
|
||||||
|
|
||||||
|
### Viewport Meta Tag
|
||||||
|
```html
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Device Testing Checklist
|
||||||
|
- [ ] iPhone SE (375px)
|
||||||
|
- [ ] iPhone 12/13 (390px)
|
||||||
|
- [ ] iPad (768px)
|
||||||
|
- [ ] Desktop (1024px+)
|
||||||
|
- [ ] Large desktop (1920px+)
|
||||||
|
- [ ] Test landscape orientations
|
||||||
|
- [ ] Test with browser zoom (125%, 150%)
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
- Designing responsive layouts
|
||||||
|
- Converting desktop designs to mobile
|
||||||
|
- Optimizing for different screen sizes
|
||||||
|
- Improving mobile user experience
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember**: Design for mobile first, then progressively enhance for larger screens!
|
||||||
53
plugin.lock.json
Normal file
53
plugin.lock.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:samuelgarrett/claude-code-plugin-test:web-dev-skills",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "56c5cef459b4503c3b5d32825ede600407e9e87c",
|
||||||
|
"treeHash": "8c43c2ec47ea536a5ce40229383c01bcc3efca0c7fdf47475f7c617cbb633bf7",
|
||||||
|
"generatedAt": "2025-11-28T10:28:07.652290Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "web-dev-skills",
|
||||||
|
"description": "Specialized skills for modern web development including React, Next.js, APIs, and frontend optimization",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "bff100eb8fa02fbe2cd59dcb6fbb59bee655450af42db8817f0ba6d62d1c6cae"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "90b4a4a8abe41065956b14a580e79344d85055bc5dfbc3bb31e489dff94efe36"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/api-designer.md",
|
||||||
|
"sha256": "45287e7bb8070f27a8a0b74e99115ad249c3d444ab218abb0ef4c663724d772e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/react-expert.md",
|
||||||
|
"sha256": "2522429c362febd3f8fecf799a178e36f7928ea1aeabdb657d99e3ad95f6cd7d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/responsive-design.md",
|
||||||
|
"sha256": "404984dfa67030407a8f131c3665671cf9415959db499a8a1035749e237ec85a"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "8c43c2ec47ea536a5ce40229383c01bcc3efca0c7fdf47475f7c617cbb633bf7"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user