@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 injectStylingTrigger() {
55
+ function injectTriggers() {
56
56
  const triggersDDir = path.join(TARGET_DIR, 'hooks', 'triggers.d');
57
- const stylingTriggerSrc = path.join(TEMPLATES_DIR, 'hooks', 'triggers.d', 'styling.json');
58
- const stylingTriggerDest = path.join(triggersDDir, 'styling.json');
57
+ if (!fs.existsSync(triggersDDir)) return false;
59
58
 
60
- if (fs.existsSync(triggersDDir) && fs.existsSync(stylingTriggerSrc)) {
61
- fs.copyFileSync(stylingTriggerSrc, stylingTriggerDest);
62
- return true;
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 false;
70
+ return injected;
65
71
  }
66
72
 
67
- function removeStylingTrigger() {
68
- const stylingTrigger = path.join(TARGET_DIR, 'hooks', 'triggers.d', 'styling.json');
69
- if (fs.existsSync(stylingTrigger)) {
70
- fs.unlinkSync(stylingTrigger);
71
- return true;
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 false;
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 styling trigger if prompt-system is installed
144
+ // Inject triggers if prompt-system is installed
133
145
  if (promptSystemExists) {
134
- if (injectStylingTrigger()) {
135
- console.log('✅ Injected styling trigger into prompt system.');
136
- console.log(' Styling-related prompts will now auto-trigger the styling skill.\n');
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 styling trigger
211
- removeStylingTrigger();
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.1.0",
4
- "description": "Claude Code UI styling system with design tokens, 36 component recipes, and visual harness",
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
- "fromp-component-system": "./bin/cli.js"
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,18 @@
1
+ {
2
+ "react-patterns": [
3
+ "react",
4
+ "component",
5
+ "useState",
6
+ "useEffect",
7
+ "useCallback",
8
+ "useMemo",
9
+ "useRef",
10
+ "useContext",
11
+ "hooks",
12
+ "render",
13
+ "gallery",
14
+ "harness",
15
+ "demo",
16
+ "showcase"
17
+ ]
18
+ }
@@ -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