@fr0mpy/component-system 2.1.0 â 2.2.0
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/bin/cli.js
CHANGED
|
@@ -52,25 +52,37 @@ function copySubdir(subdir) {
|
|
|
52
52
|
return true;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function
|
|
55
|
+
function injectTriggers() {
|
|
56
56
|
const triggersDDir = path.join(TARGET_DIR, 'hooks', 'triggers.d');
|
|
57
|
-
|
|
58
|
-
const stylingTriggerDest = path.join(triggersDDir, 'styling.json');
|
|
57
|
+
if (!fs.existsSync(triggersDDir)) return false;
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
const triggers = ['styling.json', 'react-patterns.json'];
|
|
60
|
+
let injected = false;
|
|
61
|
+
|
|
62
|
+
for (const trigger of triggers) {
|
|
63
|
+
const src = path.join(TEMPLATES_DIR, 'hooks', 'triggers.d', trigger);
|
|
64
|
+
const dest = path.join(triggersDDir, trigger);
|
|
65
|
+
if (fs.existsSync(src)) {
|
|
66
|
+
fs.copyFileSync(src, dest);
|
|
67
|
+
injected = true;
|
|
68
|
+
}
|
|
63
69
|
}
|
|
64
|
-
return
|
|
70
|
+
return injected;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
function
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
function removeTriggers() {
|
|
74
|
+
const triggersDDir = path.join(TARGET_DIR, 'hooks', 'triggers.d');
|
|
75
|
+
const triggers = ['styling.json', 'react-patterns.json'];
|
|
76
|
+
let removed = false;
|
|
77
|
+
|
|
78
|
+
for (const trigger of triggers) {
|
|
79
|
+
const triggerPath = path.join(triggersDDir, trigger);
|
|
80
|
+
if (fs.existsSync(triggerPath)) {
|
|
81
|
+
fs.unlinkSync(triggerPath);
|
|
82
|
+
removed = true;
|
|
83
|
+
}
|
|
72
84
|
}
|
|
73
|
-
return
|
|
85
|
+
return removed;
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
async function init() {
|
|
@@ -129,11 +141,11 @@ async function init() {
|
|
|
129
141
|
console.log(' đ commands/setup-styling.md');
|
|
130
142
|
console.log(' đ commands/component-harness.md\n');
|
|
131
143
|
|
|
132
|
-
// Inject
|
|
144
|
+
// Inject triggers if prompt-system is installed
|
|
133
145
|
if (promptSystemExists) {
|
|
134
|
-
if (
|
|
135
|
-
console.log('â
Injected
|
|
136
|
-
console.log(' Styling
|
|
146
|
+
if (injectTriggers()) {
|
|
147
|
+
console.log('â
Injected triggers into prompt system.');
|
|
148
|
+
console.log(' Styling and React patterns will auto-trigger on relevant prompts.\n');
|
|
137
149
|
}
|
|
138
150
|
} else {
|
|
139
151
|
console.log('âšī¸ Prompt system not detected.');
|
|
@@ -207,8 +219,8 @@ function remove() {
|
|
|
207
219
|
}
|
|
208
220
|
}
|
|
209
221
|
|
|
210
|
-
// Remove
|
|
211
|
-
|
|
222
|
+
// Remove triggers
|
|
223
|
+
removeTriggers();
|
|
212
224
|
|
|
213
225
|
// Remove styling-config.json if it exists
|
|
214
226
|
const stylingConfig = path.join(TARGET_DIR, 'styling-config.json');
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validate and track component harness completeness.
|
|
5
|
+
* Keeps the design agent on track by reporting missing components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const CLAUDE_DIR = path.join(process.cwd(), '.claude');
|
|
12
|
+
const RECIPES_DIR = path.join(CLAUDE_DIR, 'component-recipes');
|
|
13
|
+
const HARNESS_DIR = path.join(process.cwd(), 'component-harness');
|
|
14
|
+
const COMPONENTS_DIR = path.join(HARNESS_DIR, 'src', 'components');
|
|
15
|
+
|
|
16
|
+
function getRecipeNames() {
|
|
17
|
+
if (!fs.existsSync(RECIPES_DIR)) return [];
|
|
18
|
+
return fs.readdirSync(RECIPES_DIR)
|
|
19
|
+
.filter(f => f.endsWith('.md'))
|
|
20
|
+
.map(f => f.replace('.md', ''));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getGeneratedComponents() {
|
|
24
|
+
if (!fs.existsSync(COMPONENTS_DIR)) return [];
|
|
25
|
+
return fs.readdirSync(COMPONENTS_DIR)
|
|
26
|
+
.filter(f => f.endsWith('.tsx') && f !== 'index.ts')
|
|
27
|
+
.map(f => f.replace('.tsx', '').toLowerCase());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toComponentName(recipe) {
|
|
31
|
+
// kebab-case to PascalCase
|
|
32
|
+
return recipe.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function validate() {
|
|
36
|
+
console.log('Component Harness Validation\n');
|
|
37
|
+
console.log('='.repeat(40) + '\n');
|
|
38
|
+
|
|
39
|
+
// Check prerequisites
|
|
40
|
+
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
41
|
+
console.log('â .claude/ directory not found');
|
|
42
|
+
console.log(' Run: npx @fr0mpy/component-system\n');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(RECIPES_DIR)) {
|
|
47
|
+
console.log('â No component recipes found');
|
|
48
|
+
console.log(' Run: /setup-styling\n');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const recipes = getRecipeNames();
|
|
53
|
+
const generated = getGeneratedComponents();
|
|
54
|
+
|
|
55
|
+
console.log(`Recipes: ${recipes.length}`);
|
|
56
|
+
console.log(`Generated: ${generated.length}\n`);
|
|
57
|
+
|
|
58
|
+
// Find missing
|
|
59
|
+
const missing = [];
|
|
60
|
+
const present = [];
|
|
61
|
+
|
|
62
|
+
for (const recipe of recipes) {
|
|
63
|
+
const componentName = toComponentName(recipe);
|
|
64
|
+
const exists = generated.some(g =>
|
|
65
|
+
g.toLowerCase() === recipe.toLowerCase() ||
|
|
66
|
+
g.toLowerCase() === componentName.toLowerCase()
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (exists) {
|
|
70
|
+
present.push({ recipe, component: componentName });
|
|
71
|
+
} else {
|
|
72
|
+
missing.push({ recipe, component: componentName });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Report
|
|
77
|
+
if (missing.length === 0) {
|
|
78
|
+
console.log('â
All components generated!\n');
|
|
79
|
+
} else {
|
|
80
|
+
console.log(`â ī¸ Missing ${missing.length} components:\n`);
|
|
81
|
+
missing.forEach(({ recipe, component }) => {
|
|
82
|
+
console.log(` [ ] ${component} (from ${recipe}.md)`);
|
|
83
|
+
});
|
|
84
|
+
console.log('');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check Gallery.tsx imports
|
|
88
|
+
const galleryPath = path.join(HARNESS_DIR, 'src', 'Gallery.tsx');
|
|
89
|
+
if (fs.existsSync(galleryPath)) {
|
|
90
|
+
const galleryContent = fs.readFileSync(galleryPath, 'utf-8');
|
|
91
|
+
const importedComponents = [];
|
|
92
|
+
|
|
93
|
+
for (const { component } of present) {
|
|
94
|
+
if (galleryContent.includes(component)) {
|
|
95
|
+
importedComponents.push(component);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const notInGallery = present.filter(p => !importedComponents.includes(p.component));
|
|
100
|
+
|
|
101
|
+
if (notInGallery.length > 0) {
|
|
102
|
+
console.log(`â ī¸ ${notInGallery.length} components not in Gallery.tsx:\n`);
|
|
103
|
+
notInGallery.forEach(({ component }) => {
|
|
104
|
+
console.log(` [ ] ${component}`);
|
|
105
|
+
});
|
|
106
|
+
console.log('');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Summary
|
|
111
|
+
const pct = Math.round((present.length / recipes.length) * 100);
|
|
112
|
+
console.log('='.repeat(40));
|
|
113
|
+
console.log(`Progress: ${present.length}/${recipes.length} (${pct}%)\n`);
|
|
114
|
+
|
|
115
|
+
// Output for agent consumption
|
|
116
|
+
if (missing.length > 0) {
|
|
117
|
+
console.log('NEXT_ACTION: Generate missing components:');
|
|
118
|
+
console.log(missing.slice(0, 5).map(m => m.component).join(', '));
|
|
119
|
+
if (missing.length > 5) {
|
|
120
|
+
console.log(`... and ${missing.length - 5} more`);
|
|
121
|
+
}
|
|
122
|
+
console.log('');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { recipes, present, missing, total: recipes.length };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function generateStubs() {
|
|
129
|
+
const { missing } = validate();
|
|
130
|
+
|
|
131
|
+
if (missing.length === 0) {
|
|
132
|
+
console.log('No stubs needed - all components exist.\n');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log('\nGenerating stubs for missing components...\n');
|
|
137
|
+
|
|
138
|
+
fs.mkdirSync(COMPONENTS_DIR, { recursive: true });
|
|
139
|
+
|
|
140
|
+
for (const { recipe, component } of missing) {
|
|
141
|
+
const stubPath = path.join(COMPONENTS_DIR, `${component}.tsx`);
|
|
142
|
+
const stub = `// TODO: Generate from recipe: ${recipe}.md
|
|
143
|
+
import * as React from "react"
|
|
144
|
+
|
|
145
|
+
export interface ${component}Props {
|
|
146
|
+
children?: React.ReactNode
|
|
147
|
+
className?: string
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const ${component} = React.forwardRef<HTMLDivElement, ${component}Props>(
|
|
151
|
+
({ children, className, ...props }, ref) => {
|
|
152
|
+
return (
|
|
153
|
+
<div ref={ref} className={className} {...props}>
|
|
154
|
+
{/* TODO: Implement ${component} from .claude/component-recipes/${recipe}.md */}
|
|
155
|
+
<span className="text-yellow-500">[${component} - Not Implemented]</span>
|
|
156
|
+
{children}
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
${component}.displayName = "${component}"
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
fs.writeFileSync(stubPath, stub);
|
|
165
|
+
console.log(` â ${component}.tsx (stub)`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('\nStubs created. Run validation again to update Gallery.tsx\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function updateIndex() {
|
|
172
|
+
if (!fs.existsSync(COMPONENTS_DIR)) {
|
|
173
|
+
console.log('â Components directory not found\n');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const components = fs.readdirSync(COMPONENTS_DIR)
|
|
178
|
+
.filter(f => f.endsWith('.tsx'))
|
|
179
|
+
.map(f => f.replace('.tsx', ''));
|
|
180
|
+
|
|
181
|
+
const exports = components
|
|
182
|
+
.map(c => `export * from "./${c}"`)
|
|
183
|
+
.join('\n');
|
|
184
|
+
|
|
185
|
+
const indexPath = path.join(COMPONENTS_DIR, 'index.ts');
|
|
186
|
+
fs.writeFileSync(indexPath, exports + '\n');
|
|
187
|
+
|
|
188
|
+
console.log(`â
Updated index.ts with ${components.length} exports\n`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// CLI
|
|
192
|
+
const command = process.argv[2];
|
|
193
|
+
|
|
194
|
+
switch (command) {
|
|
195
|
+
case 'stubs':
|
|
196
|
+
generateStubs();
|
|
197
|
+
break;
|
|
198
|
+
case 'index':
|
|
199
|
+
updateIndex();
|
|
200
|
+
break;
|
|
201
|
+
case 'help':
|
|
202
|
+
console.log(`
|
|
203
|
+
validate-harness - Component harness validation tool
|
|
204
|
+
|
|
205
|
+
Usage:
|
|
206
|
+
npx validate-harness Validate component completeness
|
|
207
|
+
npx validate-harness stubs Generate stub files for missing components
|
|
208
|
+
npx validate-harness index Update components/index.ts exports
|
|
209
|
+
|
|
210
|
+
Run from project root (where .claude/ exists).
|
|
211
|
+
`);
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
validate();
|
|
215
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fr0mpy/component-system",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Claude Code UI styling system with design tokens,
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Claude Code UI styling system with design tokens, 40 Base UI component recipes, and visual harness",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"component-system": "./bin/cli.js",
|
|
8
|
+
"validate-harness": "./bin/validate-harness.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"test": "echo \"Tests not yet configured\""
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# React Patterns
|
|
2
|
+
|
|
3
|
+
Apply when creating, modifying, or reviewing React components.
|
|
4
|
+
|
|
5
|
+
## Rules of Hooks (CRITICAL)
|
|
6
|
+
|
|
7
|
+
Hooks must be called in the **same order** every render. Violating this crashes the app.
|
|
8
|
+
|
|
9
|
+
### Never Do
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// â Hook in callback/render prop
|
|
13
|
+
const demos = [{
|
|
14
|
+
render: () => {
|
|
15
|
+
const [state, setState] = useState() // CRASHES
|
|
16
|
+
return <Component />
|
|
17
|
+
}
|
|
18
|
+
}]
|
|
19
|
+
|
|
20
|
+
// â Hook after conditional return
|
|
21
|
+
if (loading) return <Spinner />
|
|
22
|
+
const [data, setData] = useState() // CRASHES
|
|
23
|
+
|
|
24
|
+
// â Hook in condition
|
|
25
|
+
if (userId) {
|
|
26
|
+
const [user, setUser] = useState() // CRASHES
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// â Hook in loop
|
|
30
|
+
items.map(item => {
|
|
31
|
+
const [selected, setSelected] = useState() // CRASHES
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Always Do
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// â
Extract to separate component
|
|
39
|
+
const CheckboxDemo = () => {
|
|
40
|
+
const [checked, setChecked] = useState(false)
|
|
41
|
+
return <Checkbox checked={checked} onChange={setChecked} />
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// â
Hooks before any returns
|
|
45
|
+
const [data, setData] = useState()
|
|
46
|
+
const [loading, setLoading] = useState(true)
|
|
47
|
+
if (loading) return <Spinner />
|
|
48
|
+
|
|
49
|
+
// â
Condition inside hook, not hook inside condition
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (userId) fetchUser(userId)
|
|
52
|
+
}, [userId])
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## State Updates
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// â Stale state - only increments once
|
|
59
|
+
setCount(count + 1)
|
|
60
|
+
setCount(count + 1)
|
|
61
|
+
|
|
62
|
+
// â
Functional update - increments twice
|
|
63
|
+
setCount(prev => prev + 1)
|
|
64
|
+
setCount(prev => prev + 1)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Dependencies
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
// â Missing dependency - stale closure
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
doSomething(value)
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
// â
All dependencies listed
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
doSomething(value)
|
|
78
|
+
}, [value])
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Component Galleries
|
|
82
|
+
|
|
83
|
+
When building component showcases:
|
|
84
|
+
|
|
85
|
+
| Pattern | Wrong | Right |
|
|
86
|
+
|---------|-------|-------|
|
|
87
|
+
| Stateful demo | Hook in render prop | Separate `*Demo` component |
|
|
88
|
+
| Context consumer | Render alone | Wrap in provider |
|
|
89
|
+
| Controlled input | Internal state | Props from wrapper |
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// â Wrong - hook in render function
|
|
93
|
+
const components = [{
|
|
94
|
+
name: "Checkbox",
|
|
95
|
+
render: () => {
|
|
96
|
+
const [checked, setChecked] = useState(false)
|
|
97
|
+
return <Checkbox checked={checked} />
|
|
98
|
+
}
|
|
99
|
+
}]
|
|
100
|
+
|
|
101
|
+
// â
Right - separate component
|
|
102
|
+
const CheckboxDemo = () => {
|
|
103
|
+
const [checked, setChecked] = useState(false)
|
|
104
|
+
return <Checkbox checked={checked} onChange={setChecked} />
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const components = [{
|
|
108
|
+
name: "Checkbox",
|
|
109
|
+
render: () => <CheckboxDemo />
|
|
110
|
+
}]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Cleanup
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
// â Memory leak
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const id = setInterval(tick, 1000)
|
|
119
|
+
}, [])
|
|
120
|
+
|
|
121
|
+
// â
Cleanup on unmount
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
const id = setInterval(tick, 1000)
|
|
124
|
+
return () => clearInterval(id)
|
|
125
|
+
}, [])
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Checklist
|
|
129
|
+
|
|
130
|
+
Before delivering React code:
|
|
131
|
+
|
|
132
|
+
- [ ] No hooks in callbacks, loops, conditions, or render props
|
|
133
|
+
- [ ] No hooks after conditional returns
|
|
134
|
+
- [ ] All `useEffect`/`useCallback`/`useMemo` deps included
|
|
135
|
+
- [ ] Functional state updates when depending on previous state
|
|
136
|
+
- [ ] Context consumers wrapped in their providers
|
|
137
|
+
- [ ] Effects return cleanup functions where needed
|