ai-workflow-init 6.3.2 → 6.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/output-styles/tech-explainer.md +81 -0
- package/.claude/output-styles/ui-visualizer.md +242 -0
- package/.claude/scripts/preview-component.sh +65 -0
- package/.claude/settings.json +48 -0
- package/.claude/settings.local.json +1 -46
- package/.claude/skills/react-best-practices/SKILL.md +519 -0
- package/cli.js +92 -53
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Tech Explainer Output Style
|
|
2
|
+
|
|
3
|
+
This output style is designed for explaining code, technical theories, and programming concepts.
|
|
4
|
+
|
|
5
|
+
## Activation Behavior
|
|
6
|
+
|
|
7
|
+
**CRITICAL: On first activation of this style in a session, you MUST:**
|
|
8
|
+
|
|
9
|
+
1. Use the `AskUserQuestion` tool to ask the user their experience level
|
|
10
|
+
2. Store and remember the selection for the entire session
|
|
11
|
+
3. Adapt all explanations to match the selected level
|
|
12
|
+
|
|
13
|
+
## Experience Level Selection
|
|
14
|
+
|
|
15
|
+
Trigger this question immediately when the style is activated:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Question: "What is your experience level?"
|
|
19
|
+
Header: "Level"
|
|
20
|
+
Options:
|
|
21
|
+
- Fresher: "Developer switching domains (e.g., Backend → Frontend). Familiar with programming but new to this specific area."
|
|
22
|
+
- Junior: "0-2 years experience. Still learning fundamentals and best practices."
|
|
23
|
+
- Middle: "2-5 years experience. Solid understanding, seeking deeper insights."
|
|
24
|
+
- Senior: "5+ years experience. Looking for advanced patterns and architectural considerations."
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Explanation Guidelines by Level
|
|
28
|
+
|
|
29
|
+
### Fresher (Domain Switcher)
|
|
30
|
+
- **Assume:** Strong programming fundamentals, unfamiliar with domain-specific concepts
|
|
31
|
+
- **Focus on:** Domain terminology, ecosystem differences, transferable concepts
|
|
32
|
+
- **Include:** Comparisons to concepts they likely know from other domains
|
|
33
|
+
- **Avoid:** Explaining basic programming concepts they already understand
|
|
34
|
+
- **Example:** "In React, components are similar to classes in OOP - they encapsulate state and behavior. The key difference is..."
|
|
35
|
+
|
|
36
|
+
### Junior
|
|
37
|
+
- **Assume:** Basic programming knowledge, limited practical experience
|
|
38
|
+
- **Focus on:** Step-by-step explanations, practical examples, common pitfalls
|
|
39
|
+
- **Include:** Code snippets with detailed comments, "why" behind decisions
|
|
40
|
+
- **Avoid:** Overwhelming with advanced patterns or edge cases
|
|
41
|
+
- **Example:** "Let me break this down step by step. First, we need to understand what a closure is..."
|
|
42
|
+
|
|
43
|
+
### Middle
|
|
44
|
+
- **Assume:** Solid fundamentals, ready for deeper understanding
|
|
45
|
+
- **Focus on:** Trade-offs, design decisions, performance implications
|
|
46
|
+
- **Include:** Alternative approaches, when to use what, real-world considerations
|
|
47
|
+
- **Avoid:** Over-explaining basics, but clarify when introducing advanced concepts
|
|
48
|
+
- **Example:** "There are two common approaches here. The first uses X which is simpler but has O(n²) complexity..."
|
|
49
|
+
|
|
50
|
+
### Senior
|
|
51
|
+
- **Assume:** Deep understanding, seeking nuanced insights
|
|
52
|
+
- **Focus on:** Architectural patterns, edge cases, scalability, advanced optimizations
|
|
53
|
+
- **Include:** Links to RFCs/specs, historical context, future considerations
|
|
54
|
+
- **Avoid:** Basic explanations unless specifically asked
|
|
55
|
+
- **Example:** "This pattern addresses the cache invalidation problem by using event sourcing. The trade-off is..."
|
|
56
|
+
|
|
57
|
+
## Response Format
|
|
58
|
+
|
|
59
|
+
When explaining any technical concept:
|
|
60
|
+
|
|
61
|
+
1. **Context First** - Why does this matter?
|
|
62
|
+
2. **Core Concept** - What is it? (depth varies by level)
|
|
63
|
+
3. **Practical Example** - Show it in action
|
|
64
|
+
4. **Common Mistakes** - What to avoid (for Junior/Middle)
|
|
65
|
+
5. **Advanced Considerations** - Deeper insights (for Middle/Senior)
|
|
66
|
+
|
|
67
|
+
## Session Memory
|
|
68
|
+
|
|
69
|
+
Once the experience level is selected:
|
|
70
|
+
- Remember it for all subsequent explanations in the session
|
|
71
|
+
- Do not ask again unless the user requests to change it
|
|
72
|
+
- Prefix explanations with the current mode when relevant: `[Explaining for: Middle]`
|
|
73
|
+
|
|
74
|
+
## Changing Experience Level
|
|
75
|
+
|
|
76
|
+
If the user wants to change their level mid-session, they can say:
|
|
77
|
+
- "Switch to Senior level"
|
|
78
|
+
- "Explain this for a Junior"
|
|
79
|
+
- "I'm actually a Fresher in this domain"
|
|
80
|
+
|
|
81
|
+
Update the stored preference and continue with the new level.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: UI Visualizer
|
|
3
|
+
description: Visualize UI layouts with ASCII wireframes, component trees, and detailed visual specs before implementation
|
|
4
|
+
keep-coding-instructions: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# UI Visualizer Mode
|
|
8
|
+
|
|
9
|
+
You help frontend developers visualize UI designs before implementation using text-based representations.
|
|
10
|
+
|
|
11
|
+
## Core Visualization Techniques
|
|
12
|
+
|
|
13
|
+
### 1. ASCII Wireframes (ALWAYS use for layouts)
|
|
14
|
+
|
|
15
|
+
Use box-drawing characters to represent UI structure:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─────────────────────────────────────────────────────┐
|
|
19
|
+
│ ┌─────────────────────────────────────────────┐ │
|
|
20
|
+
│ │ 🔍 Search... [🔔][👤]│ │ ← Header
|
|
21
|
+
│ └─────────────────────────────────────────────┘ │
|
|
22
|
+
├────────────┬────────────────────────────────────────┤
|
|
23
|
+
│ │ │
|
|
24
|
+
│ 📁 Home │ ┌────────┐ ┌────────┐ ┌────────┐ │
|
|
25
|
+
│ 📊 Stats │ │ Card 1 │ │ Card 2 │ │ Card 3 │ │ ← Content
|
|
26
|
+
│ ⚙️ Config │ │ │ │ │ │ │ │
|
|
27
|
+
│ │ └────────┘ └────────┘ └────────┘ │
|
|
28
|
+
│ ← Sidebar │ │
|
|
29
|
+
└────────────┴────────────────────────────────────────┘
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Component Hierarchy Tree
|
|
33
|
+
|
|
34
|
+
Always show structure:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
App
|
|
38
|
+
├── Header (h-16, sticky)
|
|
39
|
+
│ ├── Logo
|
|
40
|
+
│ ├── SearchBar (flex-1)
|
|
41
|
+
│ └── UserMenu
|
|
42
|
+
├── Sidebar (w-64, hidden@mobile)
|
|
43
|
+
│ └── NavItem[] (gap-2)
|
|
44
|
+
└── Main (flex-1, p-6)
|
|
45
|
+
└── CardGrid (grid, cols-3@lg)
|
|
46
|
+
└── Card[] (shadow-md, rounded-lg)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Visual Specs Table
|
|
50
|
+
|
|
51
|
+
For each major component, provide:
|
|
52
|
+
|
|
53
|
+
| Property | Value | Notes |
|
|
54
|
+
|----------|-------|-------|
|
|
55
|
+
| Width | 320px / 100% | Fixed on desktop, fluid on mobile |
|
|
56
|
+
| Height | auto (min 200px) | Content-driven |
|
|
57
|
+
| Padding | 24px (1.5rem) | Uses spacing scale |
|
|
58
|
+
| Border Radius | 12px | Consistent with design system |
|
|
59
|
+
| Shadow | 0 4px 6px rgba(0,0,0,0.1) | Subtle elevation |
|
|
60
|
+
| Background | #FFFFFF | --color-surface |
|
|
61
|
+
|
|
62
|
+
### 4. State Variations
|
|
63
|
+
|
|
64
|
+
Show all interactive states:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
68
|
+
│ 🔘 Default │ │ 🔘 Hover │ │ 🔘 Active │
|
|
69
|
+
│ bg: gray-100 │ │ bg: gray-200 │ │ bg: blue-500 │
|
|
70
|
+
│ text: gray-700 │ │ text: gray-900 │ │ text: white │
|
|
71
|
+
│ border: none │ │ shadow: sm │ │ shadow: inner │
|
|
72
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
73
|
+
|
|
74
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
75
|
+
│ 🔘 Disabled │ │ 🔘 Loading │
|
|
76
|
+
│ bg: gray-50 │ │ bg: gray-100 │
|
|
77
|
+
│ text: gray-300 │ │ [◌ spinner] │
|
|
78
|
+
│ cursor: not-ok │ │ opacity: 0.7 │
|
|
79
|
+
└─────────────────┘ └─────────────────┘
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 5. Responsive Breakpoints
|
|
83
|
+
|
|
84
|
+
Show layout changes:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
📱 Mobile (<640px) 📱 Tablet (640-1024px) 💻 Desktop (>1024px)
|
|
88
|
+
┌──────────────────┐ ┌─────────────────────┐ ┌──────────────────────────┐
|
|
89
|
+
│ ☰ Logo [👤] │ │ Logo [Search] [👤]│ │ Logo [ Search ] [👤]│
|
|
90
|
+
├──────────────────┤ ├──────┬──────────────┤ ├───────┬──────────────────┤
|
|
91
|
+
│ │ │ Nav │ │ │ Nav │ │
|
|
92
|
+
│ [ Card 1 ] │ │ │ [Card][Card] │ │ │ [Card][Card][Card]│
|
|
93
|
+
│ [ Card 2 ] │ │ │ [Card][Card] │ │ │ [Card][Card][Card]│
|
|
94
|
+
│ [ Card 3 ] │ │ │ │ │ │ │
|
|
95
|
+
└──────────────────┘ └──────┴──────────────┘ └───────┴──────────────────┘
|
|
96
|
+
(1 column) (2 columns) (3 columns)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 6. Color & Typography Preview
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
┌─ Color Palette ─────────────────────────────────────┐
|
|
103
|
+
│ │
|
|
104
|
+
│ Primary: ████ #3B82F6 ████ #2563EB ████ #1D4ED8│
|
|
105
|
+
│ light base dark │
|
|
106
|
+
│ │
|
|
107
|
+
│ Neutral: ░░░░ #F9FAFB ▒▒▒▒ #6B7280 ████ #111827│
|
|
108
|
+
│ 50 500 900 │
|
|
109
|
+
│ │
|
|
110
|
+
│ Semantic: ████ #10B981 ████ #F59E0B ████ #EF4444│
|
|
111
|
+
│ success warning error │
|
|
112
|
+
└──────────────────────────────────────────────────────┘
|
|
113
|
+
|
|
114
|
+
┌─ Typography Scale ──────────────────────────────────┐
|
|
115
|
+
│ xs (12px/1rem) Caption, helper text │
|
|
116
|
+
│ sm (14px/1.25rem) Body small, labels │
|
|
117
|
+
│ base (16px/1.5rem) Body text ← DEFAULT │
|
|
118
|
+
│ lg (18px/1.75rem) Lead paragraphs │
|
|
119
|
+
│ xl (20px/1.75rem) Card titles │
|
|
120
|
+
│ 2xl (24px/2rem) Section headers │
|
|
121
|
+
│ 3xl (30px/2.25rem) Page titles │
|
|
122
|
+
└──────────────────────────────────────────────────────┘
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 7. Animation & Interaction Notes
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
┌─ Interactions ──────────────────────────────────────┐
|
|
129
|
+
│ │
|
|
130
|
+
│ Hover Card: transform: translateY(-2px) │
|
|
131
|
+
│ transition: 150ms ease-out │
|
|
132
|
+
│ shadow: md → lg │
|
|
133
|
+
│ │
|
|
134
|
+
│ Button Click: scale: 1 → 0.98 → 1 │
|
|
135
|
+
│ duration: 100ms │
|
|
136
|
+
│ │
|
|
137
|
+
│ Modal Open: opacity: 0 → 1 │
|
|
138
|
+
│ transform: scale(0.95) → scale(1) │
|
|
139
|
+
│ backdrop: fade in 200ms │
|
|
140
|
+
└──────────────────────────────────────────────────────┘
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Before Writing Any UI Code
|
|
144
|
+
|
|
145
|
+
ALWAYS provide:
|
|
146
|
+
|
|
147
|
+
1. **ASCII wireframe** of the layout
|
|
148
|
+
2. **Component tree** with sizing hints
|
|
149
|
+
3. **Visual specs table** for key components
|
|
150
|
+
4. **Responsive behavior** diagram
|
|
151
|
+
5. **State variations** for interactive elements
|
|
152
|
+
|
|
153
|
+
## Communication Style
|
|
154
|
+
|
|
155
|
+
- Lead with visuals, explain after
|
|
156
|
+
- Use emoji icons (📱💻🔘✅❌) for quick scanning
|
|
157
|
+
- Include actual values (px, rem, hex codes)
|
|
158
|
+
- Show before/after for modifications
|
|
159
|
+
- Reference spacing scale (4, 8, 12, 16, 24, 32, 48...)
|
|
160
|
+
|
|
161
|
+
## Example Response Format
|
|
162
|
+
|
|
163
|
+
When asked to create a component:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
## 🎨 Login Card Visualization
|
|
167
|
+
|
|
168
|
+
### Layout
|
|
169
|
+
┌────────────────────────────────┐
|
|
170
|
+
│ Company Logo │ h: 48px
|
|
171
|
+
├────────────────────────────────┤
|
|
172
|
+
│ │
|
|
173
|
+
│ ┌──────────────────────────┐ │
|
|
174
|
+
│ │ 📧 Email │ │ input, h: 44px
|
|
175
|
+
│ └──────────────────────────┘ │
|
|
176
|
+
│ │ gap: 16px
|
|
177
|
+
│ ┌──────────────────────────┐ │
|
|
178
|
+
│ │ 🔒 Password 👁 │ │ input + toggle
|
|
179
|
+
│ └──────────────────────────┘ │
|
|
180
|
+
│ │
|
|
181
|
+
│ ☑ Remember me Forgot? → │ gap: 24px
|
|
182
|
+
│ │
|
|
183
|
+
│ ┌──────────────────────────┐ │
|
|
184
|
+
│ │ Sign In │ │ button, primary
|
|
185
|
+
│ └──────────────────────────┘ │
|
|
186
|
+
│ │
|
|
187
|
+
│ ──────── or ──────── │ divider
|
|
188
|
+
│ │
|
|
189
|
+
│ ┌──────────────────────────┐ │
|
|
190
|
+
│ │ 🔷 Continue with Google │ │ button, outline
|
|
191
|
+
│ └──────────────────────────┘ │
|
|
192
|
+
│ │
|
|
193
|
+
└────────────────────────────────┘
|
|
194
|
+
w: 400px, p: 32px
|
|
195
|
+
|
|
196
|
+
### Component Tree
|
|
197
|
+
LoginCard (w-[400px], p-8, rounded-xl, shadow-lg)
|
|
198
|
+
├── Logo (h-12, mx-auto)
|
|
199
|
+
├── Form (space-y-4)
|
|
200
|
+
│ ├── EmailInput
|
|
201
|
+
│ ├── PasswordInput (with visibility toggle)
|
|
202
|
+
│ ├── RememberRow (flex, justify-between)
|
|
203
|
+
│ └── SubmitButton (w-full, h-11)
|
|
204
|
+
├── Divider (with "or" text)
|
|
205
|
+
└── SocialButtons
|
|
206
|
+
└── GoogleButton (outline variant)
|
|
207
|
+
|
|
208
|
+
### Now implementing...
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Quick Preview Option
|
|
212
|
+
|
|
213
|
+
After presenting the UI visualization, **ALWAYS ask the user**:
|
|
214
|
+
|
|
215
|
+
> 🔍 **Quick Preview?** Would you like me to generate a temporary HTML file with Tailwind CSS CDN to preview this component in your browser?
|
|
216
|
+
>
|
|
217
|
+
> - **Yes** - I'll create a `/tmp/component-preview.html` file you can open
|
|
218
|
+
> - **No** - Continue with implementation
|
|
219
|
+
|
|
220
|
+
If user chooses **Yes**:
|
|
221
|
+
1. Generate a standalone HTML file at `/tmp/component-preview.html`
|
|
222
|
+
2. Include Tailwind CSS via CDN: `<script src="https://cdn.tailwindcss.com"></script>`
|
|
223
|
+
3. Run the preview script: `.claude/scripts/preview-component.sh /tmp/component-preview.html`
|
|
224
|
+
4. The file will auto-open in the default browser
|
|
225
|
+
|
|
226
|
+
Example preview HTML structure:
|
|
227
|
+
```html
|
|
228
|
+
<!DOCTYPE html>
|
|
229
|
+
<html lang="en">
|
|
230
|
+
<head>
|
|
231
|
+
<meta charset="UTF-8">
|
|
232
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
233
|
+
<title>Component Preview</title>
|
|
234
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
235
|
+
</head>
|
|
236
|
+
<body class="min-h-screen bg-gray-50 p-8">
|
|
237
|
+
<div class="max-w-4xl mx-auto">
|
|
238
|
+
<!-- Component code here -->
|
|
239
|
+
</div>
|
|
240
|
+
</body>
|
|
241
|
+
</html>
|
|
242
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Quick component preview - generates HTML and opens in browser
|
|
3
|
+
# Usage: ./preview-component.sh [filename.html]
|
|
4
|
+
|
|
5
|
+
FILE="${1:-/tmp/component-preview.html}"
|
|
6
|
+
|
|
7
|
+
# If no file provided, create from clipboard or stdin
|
|
8
|
+
if [ -z "$1" ]; then
|
|
9
|
+
echo "Creating preview at $FILE"
|
|
10
|
+
cat > "$FILE" << 'TEMPLATE'
|
|
11
|
+
<!DOCTYPE html>
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<head>
|
|
14
|
+
<meta charset="UTF-8">
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
16
|
+
<title>Component Preview</title>
|
|
17
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
18
|
+
<script>
|
|
19
|
+
tailwind.config = {
|
|
20
|
+
theme: {
|
|
21
|
+
extend: {
|
|
22
|
+
// Add your design tokens here
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
<style type="text/tailwindcss">
|
|
28
|
+
@layer utilities {
|
|
29
|
+
.debug * { outline: 1px solid red; }
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body class="min-h-screen bg-gray-50 p-8">
|
|
34
|
+
<div class="max-w-4xl mx-auto space-y-8">
|
|
35
|
+
|
|
36
|
+
<!-- PASTE YOUR COMPONENT HTML HERE -->
|
|
37
|
+
<div class="bg-white rounded-lg shadow-md p-6">
|
|
38
|
+
<h2 class="text-xl font-semibold">Preview Component</h2>
|
|
39
|
+
<p class="text-gray-600 mt-2">Replace this with your component code</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<!-- Debug toggle -->
|
|
45
|
+
<button
|
|
46
|
+
onclick="document.body.classList.toggle('debug')"
|
|
47
|
+
class="fixed bottom-4 right-4 bg-gray-800 text-white px-3 py-1 rounded text-sm"
|
|
48
|
+
>
|
|
49
|
+
Toggle Outlines
|
|
50
|
+
</button>
|
|
51
|
+
</body>
|
|
52
|
+
</html>
|
|
53
|
+
TEMPLATE
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Open in browser (cross-platform)
|
|
57
|
+
if command -v xdg-open &> /dev/null; then
|
|
58
|
+
xdg-open "$FILE"
|
|
59
|
+
elif command -v open &> /dev/null; then
|
|
60
|
+
open "$FILE"
|
|
61
|
+
elif command -v start &> /dev/null; then
|
|
62
|
+
start "$FILE"
|
|
63
|
+
else
|
|
64
|
+
echo "Preview ready at: $FILE"
|
|
65
|
+
fi
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup|resume|clear",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "echo REMINDER: Start EVERY response with: Skills: [list] or Skills: none"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Edit",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "echo %TOOL_INPUT% | findstr /C:\"docs/ai/planning/feature-\" >nul 2>&1 && echo [%DATE% %TIME%] Task updated in planning doc >> .claude/feature-progress.log || exit /b 0"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"Stop": [
|
|
26
|
+
{
|
|
27
|
+
"matcher": "",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "powershell.exe -NoProfile -Command \"Add-Type -AssemblyName System.Windows.Forms; [void][System.Windows.Forms.MessageBox]::Show('Task completed', 'Claude Code', 0, 64)\" 2>nul || notify-send -i dialog-information 'Claude Code' 'Task completed' 2>/dev/null || osascript -e 'display notification \"Task completed\" with title \"Claude Code\"' 2>/dev/null || exit 0"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"Notification": [
|
|
37
|
+
{
|
|
38
|
+
"matcher": "permission_prompt",
|
|
39
|
+
"hooks": [
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "powershell.exe -NoProfile -Command \"Add-Type -AssemblyName System.Windows.Forms; [void][System.Windows.Forms.MessageBox]::Show('Permission required - check terminal', 'Claude Code', 0, 48)\" 2>nul || notify-send -u critical -i dialog-warning 'Claude Code' 'Permission required' 2>/dev/null || exit 0"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -12,50 +12,5 @@
|
|
|
12
12
|
"deny": [],
|
|
13
13
|
"ask": []
|
|
14
14
|
},
|
|
15
|
-
"
|
|
16
|
-
"SessionStart": [
|
|
17
|
-
{
|
|
18
|
-
"matcher": "startup|resume|clear",
|
|
19
|
-
"hooks": [
|
|
20
|
-
{
|
|
21
|
-
"type": "command",
|
|
22
|
-
"command": "echo '⚠️ REMINDER: Start EVERY response with: 📚 Skills: [list] or 📚 Skills: none'"
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
}
|
|
26
|
-
],
|
|
27
|
-
"PostToolUse": [
|
|
28
|
-
{
|
|
29
|
-
"matcher": "Edit",
|
|
30
|
-
"hooks": [
|
|
31
|
-
{
|
|
32
|
-
"type": "command",
|
|
33
|
-
"command": "if echo \"$TOOL_INPUT\" | grep -q 'docs/ai/planning/feature-'; then echo \"[$(date '+%Y-%m-%d %H:%M:%S')] Task updated in planning doc\" >> .claude/feature-progress.log; fi"
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
}
|
|
37
|
-
],
|
|
38
|
-
"Stop": [
|
|
39
|
-
{
|
|
40
|
-
"matcher": "",
|
|
41
|
-
"hooks": [
|
|
42
|
-
{
|
|
43
|
-
"type": "command",
|
|
44
|
-
"command": "if command -v notify-send &>/dev/null; then notify-send -i dialog-information 'Claude Code' '✅ Task completed' 2>/dev/null; elif command -v powershell.exe &>/dev/null; then powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Task completed', 'Claude Code', 'OK', 'Information')\" 2>/dev/null; elif command -v osascript &>/dev/null; then osascript -e 'display notification \"Task completed\" with title \"Claude Code\"' 2>/dev/null; fi; exit 0"
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
],
|
|
49
|
-
"Notification": [
|
|
50
|
-
{
|
|
51
|
-
"matcher": "permission_prompt",
|
|
52
|
-
"hooks": [
|
|
53
|
-
{
|
|
54
|
-
"type": "command",
|
|
55
|
-
"command": "if command -v notify-send &>/dev/null; then notify-send -u critical -i dialog-warning 'Claude Code' '⚠️ Permission required - check terminal' 2>/dev/null; elif command -v powershell.exe &>/dev/null; then powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Permission required - check terminal', 'Claude Code', 'OK', 'Warning')\" 2>/dev/null; fi; exit 0"
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
]
|
|
60
|
-
}
|
|
15
|
+
"outputStyle": "default"
|
|
61
16
|
}
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-best-practices
|
|
3
|
+
description: |
|
|
4
|
+
React and Next.js performance optimization guidelines from Vercel Engineering.
|
|
5
|
+
Contains 45 rules across 8 categories, prioritized by impact.
|
|
6
|
+
|
|
7
|
+
ALWAYS load when working with React/Next.js code:
|
|
8
|
+
- Writing new React components or Next.js pages
|
|
9
|
+
- Implementing data fetching (client or server-side)
|
|
10
|
+
- Reviewing code for performance issues
|
|
11
|
+
- Refactoring existing React/Next.js code
|
|
12
|
+
- Optimizing bundle size or load times
|
|
13
|
+
- Working with async operations and Promise handling
|
|
14
|
+
- Implementing Suspense boundaries or streaming
|
|
15
|
+
|
|
16
|
+
Keywords: React, Next.js, performance, optimization, bundle, async, Promise,
|
|
17
|
+
waterfall, rerender, memo, useMemo, useCallback, Suspense, RSC,
|
|
18
|
+
server components, client components, SWR, data fetching, cache
|
|
19
|
+
|
|
20
|
+
Do NOT load for:
|
|
21
|
+
- Non-React projects (Vue, Angular, Svelte, etc.)
|
|
22
|
+
- Backend-only code without React integration
|
|
23
|
+
- Pure CSS/styling work (use frontend-design-fundamentals)
|
|
24
|
+
|
|
25
|
+
Works with other skills:
|
|
26
|
+
- frontend-design-fundamentals: For UI/styling best practices
|
|
27
|
+
- quality-code-check: For linting and type checking
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
# React Best Practices
|
|
31
|
+
|
|
32
|
+
Comprehensive performance optimization guide for React and Next.js applications.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Rule Categories by Priority
|
|
37
|
+
|
|
38
|
+
| Priority | Category | Impact | Prefix |
|
|
39
|
+
|----------|----------|--------|--------|
|
|
40
|
+
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
|
41
|
+
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
|
42
|
+
| 3 | Server-Side Performance | HIGH | `server-` |
|
|
43
|
+
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
|
44
|
+
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
|
45
|
+
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
|
46
|
+
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
|
47
|
+
| 8 | Advanced Patterns | LOW | `advanced-` |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 1. Eliminating Waterfalls (CRITICAL)
|
|
52
|
+
|
|
53
|
+
Waterfalls are the #1 performance killer. Each sequential await adds full network latency.
|
|
54
|
+
|
|
55
|
+
### Promise.all() for Independent Operations
|
|
56
|
+
|
|
57
|
+
**Impact:** 2-10× improvement
|
|
58
|
+
|
|
59
|
+
When async operations have no interdependencies, execute them concurrently.
|
|
60
|
+
|
|
61
|
+
**❌ Incorrect (sequential execution, 3 round trips):**
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const user = await fetchUser()
|
|
65
|
+
const posts = await fetchPosts()
|
|
66
|
+
const comments = await fetchComments()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**✅ Correct (parallel execution, 1 round trip):**
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const [user, posts, comments] = await Promise.all([
|
|
73
|
+
fetchUser(),
|
|
74
|
+
fetchPosts(),
|
|
75
|
+
fetchComments()
|
|
76
|
+
])
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Strategic Suspense Boundaries
|
|
80
|
+
|
|
81
|
+
**Impact:** Faster initial paint
|
|
82
|
+
|
|
83
|
+
Instead of awaiting data before returning JSX, use Suspense to show wrapper UI faster.
|
|
84
|
+
|
|
85
|
+
**❌ Incorrect (wrapper blocked by data fetching):**
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
async function Page() {
|
|
89
|
+
const data = await fetchData() // Blocks entire page
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div>
|
|
93
|
+
<div>Sidebar</div>
|
|
94
|
+
<div>Header</div>
|
|
95
|
+
<div>
|
|
96
|
+
<DataDisplay data={data} />
|
|
97
|
+
</div>
|
|
98
|
+
<div>Footer</div>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**✅ Correct (wrapper shows immediately, data streams in):**
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
function Page() {
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<div>Sidebar</div>
|
|
111
|
+
<div>Header</div>
|
|
112
|
+
<div>
|
|
113
|
+
<Suspense fallback={<Skeleton />}>
|
|
114
|
+
<DataDisplay />
|
|
115
|
+
</Suspense>
|
|
116
|
+
</div>
|
|
117
|
+
<div>Footer</div>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function DataDisplay() {
|
|
123
|
+
const data = await fetchData() // Only blocks this component
|
|
124
|
+
return <div>{data.content}</div>
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Alternative (share promise across components):**
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
function Page() {
|
|
132
|
+
const dataPromise = fetchData() // Start fetch immediately, don't await
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div>
|
|
136
|
+
<div>Sidebar</div>
|
|
137
|
+
<Suspense fallback={<Skeleton />}>
|
|
138
|
+
<DataDisplay dataPromise={dataPromise} />
|
|
139
|
+
<DataSummary dataPromise={dataPromise} />
|
|
140
|
+
</Suspense>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
|
146
|
+
const data = use(dataPromise) // Unwraps the promise
|
|
147
|
+
return <div>{data.content}</div>
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 2. Bundle Size Optimization (CRITICAL)
|
|
154
|
+
|
|
155
|
+
Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
|
|
156
|
+
|
|
157
|
+
### Avoid Barrel File Imports
|
|
158
|
+
|
|
159
|
+
**Impact:** 200-800ms import cost, slow builds
|
|
160
|
+
|
|
161
|
+
Import directly from source files instead of barrel files.
|
|
162
|
+
|
|
163
|
+
**❌ Incorrect (imports entire library):**
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
import { Check, X, Menu } from 'lucide-react'
|
|
167
|
+
// Loads 1,583 modules, takes ~2.8s extra in dev
|
|
168
|
+
|
|
169
|
+
import { Button, TextField } from '@mui/material'
|
|
170
|
+
// Loads 2,225 modules, takes ~4.2s extra in dev
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**✅ Correct (imports only what you need):**
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import Check from 'lucide-react/dist/esm/icons/check'
|
|
177
|
+
import X from 'lucide-react/dist/esm/icons/x'
|
|
178
|
+
import Menu from 'lucide-react/dist/esm/icons/menu'
|
|
179
|
+
|
|
180
|
+
import Button from '@mui/material/Button'
|
|
181
|
+
import TextField from '@mui/material/TextField'
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Alternative (Next.js 13.5+):**
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
// next.config.js
|
|
188
|
+
module.exports = {
|
|
189
|
+
experimental: {
|
|
190
|
+
optimizePackageImports: ['lucide-react', '@mui/material']
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Commonly affected libraries:** `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `date-fns`.
|
|
196
|
+
|
|
197
|
+
### Dynamic Imports for Heavy Components
|
|
198
|
+
|
|
199
|
+
**Impact:** Directly affects TTI and LCP
|
|
200
|
+
|
|
201
|
+
Use `next/dynamic` to lazy-load large components not needed on initial render.
|
|
202
|
+
|
|
203
|
+
**❌ Incorrect (Monaco bundles with main chunk ~300KB):**
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
import { MonacoEditor } from './monaco-editor'
|
|
207
|
+
|
|
208
|
+
function CodePanel({ code }: { code: string }) {
|
|
209
|
+
return <MonacoEditor value={code} />
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**✅ Correct (Monaco loads on demand):**
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
import dynamic from 'next/dynamic'
|
|
217
|
+
|
|
218
|
+
const MonacoEditor = dynamic(
|
|
219
|
+
() => import('./monaco-editor').then(m => m.MonacoEditor),
|
|
220
|
+
{ ssr: false }
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
function CodePanel({ code }: { code: string }) {
|
|
224
|
+
return <MonacoEditor value={code} />
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 3. Server-Side Performance (HIGH)
|
|
231
|
+
|
|
232
|
+
Optimizing server-side rendering reduces response times.
|
|
233
|
+
|
|
234
|
+
### Per-Request Deduplication with React.cache()
|
|
235
|
+
|
|
236
|
+
**Impact:** Deduplicates within request
|
|
237
|
+
|
|
238
|
+
Use `React.cache()` for server-side request deduplication.
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { cache } from 'react'
|
|
242
|
+
|
|
243
|
+
export const getCurrentUser = cache(async () => {
|
|
244
|
+
const session = await auth()
|
|
245
|
+
if (!session?.user?.id) return null
|
|
246
|
+
return await db.user.findUnique({
|
|
247
|
+
where: { id: session.user.id }
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Avoid inline objects as arguments** - use primitives for cache hits:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// ❌ Always cache miss
|
|
256
|
+
const getUser = cache(async (params: { uid: number }) => {...})
|
|
257
|
+
getUser({ uid: 1 })
|
|
258
|
+
getUser({ uid: 1 }) // Cache miss, runs query again
|
|
259
|
+
|
|
260
|
+
// ✅ Cache hit
|
|
261
|
+
const getUser = cache(async (uid: number) => {...})
|
|
262
|
+
getUser(1)
|
|
263
|
+
getUser(1) // Cache hit, returns cached result
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Note:** Next.js `fetch` has automatic memoization. Use `React.cache()` for:
|
|
267
|
+
- Database queries (Prisma, Drizzle, etc.)
|
|
268
|
+
- Heavy computations
|
|
269
|
+
- Authentication checks
|
|
270
|
+
- File system operations
|
|
271
|
+
|
|
272
|
+
### Parallel Data Fetching with Component Composition
|
|
273
|
+
|
|
274
|
+
**Impact:** Eliminates server-side waterfalls
|
|
275
|
+
|
|
276
|
+
React Server Components execute sequentially within a tree. Restructure with composition to parallelize.
|
|
277
|
+
|
|
278
|
+
**❌ Incorrect (Sidebar waits for Page's fetch):**
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
export default async function Page() {
|
|
282
|
+
const header = await fetchHeader()
|
|
283
|
+
return (
|
|
284
|
+
<div>
|
|
285
|
+
<div>{header}</div>
|
|
286
|
+
<Sidebar />
|
|
287
|
+
</div>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function Sidebar() {
|
|
292
|
+
const items = await fetchSidebarItems()
|
|
293
|
+
return <nav>{items.map(renderItem)}</nav>
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**✅ Correct (both fetch simultaneously):**
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
async function Header() {
|
|
301
|
+
const data = await fetchHeader()
|
|
302
|
+
return <div>{data}</div>
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function Sidebar() {
|
|
306
|
+
const items = await fetchSidebarItems()
|
|
307
|
+
return <nav>{items.map(renderItem)}</nav>
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export default function Page() {
|
|
311
|
+
return (
|
|
312
|
+
<div>
|
|
313
|
+
<Header />
|
|
314
|
+
<Sidebar />
|
|
315
|
+
</div>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## 4. Client-Side Data Fetching (MEDIUM-HIGH)
|
|
323
|
+
|
|
324
|
+
### Use SWR for Automatic Deduplication
|
|
325
|
+
|
|
326
|
+
**Impact:** Automatic deduplication and caching
|
|
327
|
+
|
|
328
|
+
**❌ Incorrect (no deduplication, each instance fetches):**
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
function UserList() {
|
|
332
|
+
const [users, setUsers] = useState([])
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
fetch('/api/users')
|
|
335
|
+
.then(r => r.json())
|
|
336
|
+
.then(setUsers)
|
|
337
|
+
}, [])
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**✅ Correct (multiple instances share one request):**
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
import useSWR from 'swr'
|
|
345
|
+
|
|
346
|
+
function UserList() {
|
|
347
|
+
const { data: users } = useSWR('/api/users', fetcher)
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## 5. Re-render Optimization (MEDIUM)
|
|
354
|
+
|
|
355
|
+
Reducing unnecessary re-renders minimizes wasted computation.
|
|
356
|
+
|
|
357
|
+
### Extract to Memoized Components
|
|
358
|
+
|
|
359
|
+
**Impact:** Enables early returns
|
|
360
|
+
|
|
361
|
+
**❌ Incorrect (computes avatar even when loading):**
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
function Profile({ user, loading }: Props) {
|
|
365
|
+
const avatar = useMemo(() => {
|
|
366
|
+
const id = computeAvatarId(user)
|
|
367
|
+
return <Avatar id={id} />
|
|
368
|
+
}, [user])
|
|
369
|
+
|
|
370
|
+
if (loading) return <Skeleton />
|
|
371
|
+
return <div>{avatar}</div>
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**✅ Correct (skips computation when loading):**
|
|
376
|
+
|
|
377
|
+
```tsx
|
|
378
|
+
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
|
379
|
+
const id = useMemo(() => computeAvatarId(user), [user])
|
|
380
|
+
return <Avatar id={id} />
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
function Profile({ user, loading }: Props) {
|
|
384
|
+
if (loading) return <Skeleton />
|
|
385
|
+
return (
|
|
386
|
+
<div>
|
|
387
|
+
<UserAvatar user={user} />
|
|
388
|
+
</div>
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Note:** If React Compiler is enabled, manual memoization is not necessary.
|
|
394
|
+
|
|
395
|
+
### Subscribe to Derived State
|
|
396
|
+
|
|
397
|
+
**Impact:** Reduces re-render frequency
|
|
398
|
+
|
|
399
|
+
**❌ Incorrect (re-renders on every pixel change):**
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
function Sidebar() {
|
|
403
|
+
const width = useWindowWidth() // updates continuously
|
|
404
|
+
const isMobile = width < 768
|
|
405
|
+
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**✅ Correct (re-renders only when boolean changes):**
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
function Sidebar() {
|
|
413
|
+
const isMobile = useMediaQuery('(max-width: 767px)')
|
|
414
|
+
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 6. Rendering Performance (MEDIUM)
|
|
421
|
+
|
|
422
|
+
### Use Explicit Conditional Rendering
|
|
423
|
+
|
|
424
|
+
**Impact:** Prevents rendering 0 or NaN
|
|
425
|
+
|
|
426
|
+
**❌ Incorrect (renders "0" when count is 0):**
|
|
427
|
+
|
|
428
|
+
```tsx
|
|
429
|
+
function Badge({ count }: { count: number }) {
|
|
430
|
+
return (
|
|
431
|
+
<div>
|
|
432
|
+
{count && <span className="badge">{count}</span>}
|
|
433
|
+
</div>
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
// When count = 0, renders: <div>0</div>
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**✅ Correct (renders nothing when count is 0):**
|
|
440
|
+
|
|
441
|
+
```tsx
|
|
442
|
+
function Badge({ count }: { count: number }) {
|
|
443
|
+
return (
|
|
444
|
+
<div>
|
|
445
|
+
{count > 0 ? <span className="badge">{count}</span> : null}
|
|
446
|
+
</div>
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## 7. JavaScript Performance (LOW-MEDIUM)
|
|
454
|
+
|
|
455
|
+
Micro-optimizations for hot paths.
|
|
456
|
+
|
|
457
|
+
### Build Index Maps for Repeated Lookups
|
|
458
|
+
|
|
459
|
+
**Impact:** 1M ops → 2K ops
|
|
460
|
+
|
|
461
|
+
**❌ Incorrect (O(n) per lookup):**
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
function processOrders(orders: Order[], users: User[]) {
|
|
465
|
+
return orders.map(order => ({
|
|
466
|
+
...order,
|
|
467
|
+
user: users.find(u => u.id === order.userId)
|
|
468
|
+
}))
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**✅ Correct (O(1) per lookup):**
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
function processOrders(orders: Order[], users: User[]) {
|
|
476
|
+
const userById = new Map(users.map(u => [u.id, u]))
|
|
477
|
+
|
|
478
|
+
return orders.map(order => ({
|
|
479
|
+
...order,
|
|
480
|
+
user: userById.get(order.userId)
|
|
481
|
+
}))
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Quick Reference Checklist
|
|
488
|
+
|
|
489
|
+
### CRITICAL Priority
|
|
490
|
+
- [ ] Use `Promise.all()` for independent async operations
|
|
491
|
+
- [ ] Avoid barrel file imports (or use `optimizePackageImports`)
|
|
492
|
+
- [ ] Use `next/dynamic` for heavy components
|
|
493
|
+
- [ ] Defer third-party scripts (analytics, logging)
|
|
494
|
+
|
|
495
|
+
### HIGH Priority
|
|
496
|
+
- [ ] Use `React.cache()` for server-side deduplication
|
|
497
|
+
- [ ] Structure RSC for parallel data fetching
|
|
498
|
+
- [ ] Use Suspense boundaries strategically
|
|
499
|
+
|
|
500
|
+
### MEDIUM Priority
|
|
501
|
+
- [ ] Use SWR for client-side data fetching
|
|
502
|
+
- [ ] Extract expensive work to memoized components
|
|
503
|
+
- [ ] Subscribe to derived booleans, not raw values
|
|
504
|
+
- [ ] Use explicit ternary for conditionals with numbers
|
|
505
|
+
|
|
506
|
+
### LOW Priority
|
|
507
|
+
- [ ] Build Maps for repeated lookups
|
|
508
|
+
- [ ] Cache property access in loops
|
|
509
|
+
- [ ] Use Set/Map for O(1) lookups
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## References
|
|
514
|
+
|
|
515
|
+
- [Vercel React Best Practices Repository](https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices)
|
|
516
|
+
- [React.cache documentation](https://react.dev/reference/react/cache)
|
|
517
|
+
- [SWR documentation](https://swr.vercel.app)
|
|
518
|
+
- [Next.js Package Imports Optimization](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
|
|
519
|
+
- [React Compiler](https://react.dev/learn/react-compiler)
|
package/cli.js
CHANGED
|
@@ -212,7 +212,7 @@ const AI_TOOLS = [
|
|
|
212
212
|
id: "claude",
|
|
213
213
|
name: "Claude Code",
|
|
214
214
|
description: "Anthropic's AI coding assistant",
|
|
215
|
-
folders: [".claude/commands", ".claude/skills", ".claude/themes"],
|
|
215
|
+
folders: [".claude/commands", ".claude/skills", ".claude/themes", ".claude/output-styles", ".claude/agents"],
|
|
216
216
|
},
|
|
217
217
|
{
|
|
218
218
|
id: "opencode",
|
|
@@ -399,62 +399,18 @@ function installClaudeCode() {
|
|
|
399
399
|
run(`wget -qO ${claudeMdPath} ${RAW_BASE}/.claude/CLAUDE.md`);
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
//
|
|
403
|
-
step("🚚
|
|
402
|
+
// Download settings.json from repo (project-level, shareable with team)
|
|
403
|
+
step("🚚 Downloading Claude Code hooks (.claude/settings.json)...");
|
|
404
404
|
const claudeSettingsPath = ".claude/settings.json";
|
|
405
405
|
if (existsSync(claudeSettingsPath)) {
|
|
406
406
|
skip(`Skipping (already exists): ${claudeSettingsPath}`);
|
|
407
407
|
} else {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
{
|
|
415
|
-
type: "command",
|
|
416
|
-
command: "echo '⚠️ REMINDER: Start EVERY response with: 📚 Skills: [list] or 📚 Skills: none'"
|
|
417
|
-
}
|
|
418
|
-
]
|
|
419
|
-
}
|
|
420
|
-
],
|
|
421
|
-
PostToolUse: [
|
|
422
|
-
{
|
|
423
|
-
matcher: "Edit",
|
|
424
|
-
hooks: [
|
|
425
|
-
{
|
|
426
|
-
type: "command",
|
|
427
|
-
command: "if echo \"$TOOL_INPUT\" | grep -q 'docs/ai/planning/feature-'; then echo \"[$(date '+%Y-%m-%d %H:%M:%S')] Task updated in planning doc\" >> .claude/feature-progress.log; fi"
|
|
428
|
-
}
|
|
429
|
-
]
|
|
430
|
-
}
|
|
431
|
-
],
|
|
432
|
-
Stop: [
|
|
433
|
-
{
|
|
434
|
-
matcher: "",
|
|
435
|
-
hooks: [
|
|
436
|
-
{
|
|
437
|
-
type: "command",
|
|
438
|
-
command: "if command -v notify-send &>/dev/null; then notify-send -i dialog-information 'Claude Code' '✅ Task completed' 2>/dev/null; elif command -v powershell.exe &>/dev/null; then powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Task completed', 'Claude Code', 'OK', 'Information')\" 2>/dev/null; elif command -v osascript &>/dev/null; then osascript -e 'display notification \"Task completed\" with title \"Claude Code\"' 2>/dev/null; fi; exit 0"
|
|
439
|
-
}
|
|
440
|
-
]
|
|
441
|
-
}
|
|
442
|
-
],
|
|
443
|
-
Notification: [
|
|
444
|
-
{
|
|
445
|
-
matcher: "permission_prompt",
|
|
446
|
-
hooks: [
|
|
447
|
-
{
|
|
448
|
-
type: "command",
|
|
449
|
-
command: "if command -v notify-send &>/dev/null; then notify-send -u critical -i dialog-warning 'Claude Code' '⚠️ Permission required - check terminal' 2>/dev/null; elif command -v powershell.exe &>/dev/null; then powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Permission required - check terminal', 'Claude Code', 'OK', 'Warning')\" 2>/dev/null; fi; exit 0"
|
|
450
|
-
}
|
|
451
|
-
]
|
|
452
|
-
}
|
|
453
|
-
]
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2));
|
|
457
|
-
success(`Created: ${claudeSettingsPath}`);
|
|
408
|
+
try {
|
|
409
|
+
run(`curl -fsSL ${RAW_BASE}/.claude/settings.json -o ${claudeSettingsPath}`);
|
|
410
|
+
} catch (_) {
|
|
411
|
+
run(`wget -qO ${claudeSettingsPath} ${RAW_BASE}/.claude/settings.json`);
|
|
412
|
+
}
|
|
413
|
+
success(`Downloaded: ${claudeSettingsPath}`);
|
|
458
414
|
}
|
|
459
415
|
|
|
460
416
|
// Download skills folder (always overwrite to get latest)
|
|
@@ -470,6 +426,20 @@ function installClaudeCode() {
|
|
|
470
426
|
mkdirSync(".claude/themes", { recursive: true });
|
|
471
427
|
}
|
|
472
428
|
run(`npx degit ${REPO}/.claude/themes .claude/themes --force`);
|
|
429
|
+
|
|
430
|
+
// Download output-styles folder (always overwrite to get latest)
|
|
431
|
+
step("🚚 Downloading Claude Code output-styles (.claude/output-styles)...");
|
|
432
|
+
if (!existsSync(".claude/output-styles")) {
|
|
433
|
+
mkdirSync(".claude/output-styles", { recursive: true });
|
|
434
|
+
}
|
|
435
|
+
run(`npx degit ${REPO}/.claude/output-styles .claude/output-styles --force`);
|
|
436
|
+
|
|
437
|
+
// Download agents folder (always overwrite to get latest)
|
|
438
|
+
step("🚚 Downloading Claude Code agents (.claude/agents)...");
|
|
439
|
+
if (!existsSync(".claude/agents")) {
|
|
440
|
+
mkdirSync(".claude/agents", { recursive: true });
|
|
441
|
+
}
|
|
442
|
+
run(`npx degit ${REPO}/.claude/agents .claude/agents --force`);
|
|
473
443
|
}
|
|
474
444
|
|
|
475
445
|
// Install OpenCode
|
|
@@ -568,6 +538,66 @@ function installFactoryDroid() {
|
|
|
568
538
|
}
|
|
569
539
|
}
|
|
570
540
|
|
|
541
|
+
// Check and clone missing .factory subfolders (when .factory already exists)
|
|
542
|
+
function checkAndCloneFactory() {
|
|
543
|
+
if (!existsSync(".factory")) {
|
|
544
|
+
return; // .factory doesn't exist, skip
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
step("🔍 Checking for missing .factory subfolders...");
|
|
548
|
+
|
|
549
|
+
const factorySubfolders = [
|
|
550
|
+
{ path: ".factory/commands", name: "commands" },
|
|
551
|
+
{ path: ".factory/droids", name: "droids" },
|
|
552
|
+
{ path: ".factory/skills", name: "skills" },
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
for (const subfolder of factorySubfolders) {
|
|
556
|
+
if (!existsSync(subfolder.path)) {
|
|
557
|
+
step(`🚚 Cloning missing subfolder: ${subfolder.path}...`);
|
|
558
|
+
mkdirSync(subfolder.path, { recursive: true });
|
|
559
|
+
try {
|
|
560
|
+
run(`npx degit ${REPO}/${subfolder.path} ${subfolder.path} --force`);
|
|
561
|
+
success(`Cloned: ${subfolder.path}`);
|
|
562
|
+
} catch (e) {
|
|
563
|
+
console.log(`${colors.yellow}⚠️ ${subfolder.path} not found in repo${colors.reset}`);
|
|
564
|
+
}
|
|
565
|
+
} else {
|
|
566
|
+
skip(`Already exists: ${subfolder.path}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Check and clone missing .opencode subfolders (when .opencode already exists)
|
|
572
|
+
function checkAndCloneOpenCode() {
|
|
573
|
+
if (!existsSync(".opencode")) {
|
|
574
|
+
return; // .opencode doesn't exist, skip
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
step("🔍 Checking for missing .opencode subfolders...");
|
|
578
|
+
|
|
579
|
+
const opencodeSubfolders = [
|
|
580
|
+
{ path: ".opencode/agent", name: "agent" },
|
|
581
|
+
{ path: ".opencode/command", name: "command" },
|
|
582
|
+
{ path: ".opencode/skill", name: "skill" },
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
for (const subfolder of opencodeSubfolders) {
|
|
586
|
+
if (!existsSync(subfolder.path)) {
|
|
587
|
+
step(`🚚 Cloning missing subfolder: ${subfolder.path}...`);
|
|
588
|
+
mkdirSync(subfolder.path, { recursive: true });
|
|
589
|
+
try {
|
|
590
|
+
run(`npx degit ${REPO}/${subfolder.path} ${subfolder.path} --force`);
|
|
591
|
+
success(`Cloned: ${subfolder.path}`);
|
|
592
|
+
} catch (e) {
|
|
593
|
+
console.log(`${colors.yellow}⚠️ ${subfolder.path} not found in repo${colors.reset}`);
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
skip(`Already exists: ${subfolder.path}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
571
601
|
async function main() {
|
|
572
602
|
console.log(`
|
|
573
603
|
${colors.cyan}╔═══════════════════════════════════════════════════════════╗
|
|
@@ -640,6 +670,15 @@ ${colors.cyan}╔═════════════════════
|
|
|
640
670
|
installFactoryDroid();
|
|
641
671
|
}
|
|
642
672
|
|
|
673
|
+
// Check and clone missing subfolders for existing .factory and .opencode
|
|
674
|
+
// This runs regardless of tool selection, in case folders already exist
|
|
675
|
+
if (!toolIds.includes("factory")) {
|
|
676
|
+
checkAndCloneFactory();
|
|
677
|
+
}
|
|
678
|
+
if (!toolIds.includes("opencode")) {
|
|
679
|
+
checkAndCloneOpenCode();
|
|
680
|
+
}
|
|
681
|
+
|
|
643
682
|
// Download AGENTS.md (luôn ghi đè)
|
|
644
683
|
step("🚚 Downloading AGENTS.md...");
|
|
645
684
|
try {
|