real-prototypes-skill 0.1.1 → 0.1.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/skills/real-prototypes-skill/SKILL.md +212 -16
- package/.claude/skills/real-prototypes-skill/cli.js +523 -17
- package/.claude/skills/real-prototypes-skill/scripts/detect-prototype.js +652 -0
- package/.claude/skills/real-prototypes-skill/scripts/extract-components.js +731 -0
- package/.claude/skills/real-prototypes-skill/scripts/extract-css.js +557 -0
- package/.claude/skills/real-prototypes-skill/scripts/generate-plan.js +744 -0
- package/.claude/skills/real-prototypes-skill/scripts/html-to-react.js +645 -0
- package/.claude/skills/real-prototypes-skill/scripts/inject-component.js +604 -0
- package/.claude/skills/real-prototypes-skill/scripts/project-structure.js +457 -0
- package/.claude/skills/real-prototypes-skill/scripts/visual-diff.js +474 -0
- package/.claude/skills/real-prototypes-skill/validation/color-validator.js +496 -0
- package/bin/cli.js +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Component Library Extractor
|
|
5
|
+
*
|
|
6
|
+
* Analyzes captured HTML files to identify common UI patterns and
|
|
7
|
+
* generates a reusable component library from the captured platform.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Identifies common UI patterns (buttons, cards, inputs, tables)
|
|
11
|
+
* - Extracts component variants (primary, secondary, destructive)
|
|
12
|
+
* - Generates React component files with exact styling
|
|
13
|
+
* - Creates component registry for easy lookup
|
|
14
|
+
* - Tracks component source for reference
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node extract-components.js --project <name>
|
|
18
|
+
* node extract-components.js --input ./html --output ./components
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { JSDOM } = require('jsdom');
|
|
24
|
+
|
|
25
|
+
// Component detection patterns
|
|
26
|
+
const COMPONENT_PATTERNS = {
|
|
27
|
+
Button: {
|
|
28
|
+
selectors: [
|
|
29
|
+
'button',
|
|
30
|
+
'[role="button"]',
|
|
31
|
+
'a.btn',
|
|
32
|
+
'a.button',
|
|
33
|
+
'[class*="btn"]',
|
|
34
|
+
'[class*="button"]'
|
|
35
|
+
],
|
|
36
|
+
excludeSelectors: ['[type="submit"]', '[type="reset"]'],
|
|
37
|
+
variants: {
|
|
38
|
+
primary: [/primary/i, /btn-primary/i, /bg-blue/i, /bg-primary/i],
|
|
39
|
+
secondary: [/secondary/i, /btn-secondary/i, /btn-outline/i, /btn-ghost/i],
|
|
40
|
+
destructive: [/danger/i, /destructive/i, /btn-danger/i, /btn-red/i, /delete/i],
|
|
41
|
+
disabled: [/disabled/i, /btn-disabled/i]
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
Card: {
|
|
45
|
+
selectors: [
|
|
46
|
+
'[class*="card"]',
|
|
47
|
+
'[class*="panel"]',
|
|
48
|
+
'[class*="tile"]',
|
|
49
|
+
'[class*="box"]',
|
|
50
|
+
'article'
|
|
51
|
+
],
|
|
52
|
+
variants: {
|
|
53
|
+
default: [/card(?!-)/i],
|
|
54
|
+
elevated: [/shadow/i, /elevated/i],
|
|
55
|
+
bordered: [/border/i, /outlined/i]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
Input: {
|
|
59
|
+
selectors: [
|
|
60
|
+
'input[type="text"]',
|
|
61
|
+
'input[type="email"]',
|
|
62
|
+
'input[type="password"]',
|
|
63
|
+
'input[type="search"]',
|
|
64
|
+
'input[type="tel"]',
|
|
65
|
+
'input[type="url"]',
|
|
66
|
+
'input:not([type])',
|
|
67
|
+
'textarea',
|
|
68
|
+
'[class*="input"]',
|
|
69
|
+
'[class*="text-field"]'
|
|
70
|
+
],
|
|
71
|
+
variants: {
|
|
72
|
+
default: [/input(?!-)/i],
|
|
73
|
+
error: [/error/i, /invalid/i, /danger/i],
|
|
74
|
+
success: [/success/i, /valid/i],
|
|
75
|
+
disabled: [/disabled/i]
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
Select: {
|
|
79
|
+
selectors: [
|
|
80
|
+
'select',
|
|
81
|
+
'[class*="select"]',
|
|
82
|
+
'[class*="dropdown"]',
|
|
83
|
+
'[role="listbox"]'
|
|
84
|
+
],
|
|
85
|
+
variants: {
|
|
86
|
+
default: [/select(?!-)/i],
|
|
87
|
+
multiple: [/multiple/i]
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
Table: {
|
|
91
|
+
selectors: [
|
|
92
|
+
'table',
|
|
93
|
+
'[class*="table"]',
|
|
94
|
+
'[class*="data-grid"]',
|
|
95
|
+
'[role="grid"]'
|
|
96
|
+
],
|
|
97
|
+
variants: {
|
|
98
|
+
default: [/table(?!-)/i],
|
|
99
|
+
striped: [/striped/i, /zebra/i],
|
|
100
|
+
bordered: [/bordered/i]
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
Badge: {
|
|
104
|
+
selectors: [
|
|
105
|
+
'[class*="badge"]',
|
|
106
|
+
'[class*="tag"]',
|
|
107
|
+
'[class*="chip"]',
|
|
108
|
+
'[class*="label"]',
|
|
109
|
+
'[class*="pill"]'
|
|
110
|
+
],
|
|
111
|
+
variants: {
|
|
112
|
+
default: [/badge(?!-)/i],
|
|
113
|
+
success: [/success/i, /green/i],
|
|
114
|
+
warning: [/warning/i, /yellow/i, /orange/i],
|
|
115
|
+
error: [/error/i, /danger/i, /red/i],
|
|
116
|
+
info: [/info/i, /blue/i]
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
Avatar: {
|
|
120
|
+
selectors: [
|
|
121
|
+
'[class*="avatar"]',
|
|
122
|
+
'[class*="profile-image"]',
|
|
123
|
+
'[class*="user-image"]',
|
|
124
|
+
'img[class*="rounded-full"]'
|
|
125
|
+
],
|
|
126
|
+
variants: {
|
|
127
|
+
default: [/avatar(?!-)/i],
|
|
128
|
+
small: [/sm/i, /small/i, /xs/i],
|
|
129
|
+
large: [/lg/i, /large/i, /xl/i]
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
Modal: {
|
|
133
|
+
selectors: [
|
|
134
|
+
'[class*="modal"]',
|
|
135
|
+
'[class*="dialog"]',
|
|
136
|
+
'[role="dialog"]',
|
|
137
|
+
'[aria-modal="true"]'
|
|
138
|
+
],
|
|
139
|
+
variants: {
|
|
140
|
+
default: [/modal(?!-)/i]
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
Alert: {
|
|
144
|
+
selectors: [
|
|
145
|
+
'[class*="alert"]',
|
|
146
|
+
'[class*="notification"]',
|
|
147
|
+
'[class*="toast"]',
|
|
148
|
+
'[role="alert"]'
|
|
149
|
+
],
|
|
150
|
+
variants: {
|
|
151
|
+
success: [/success/i, /green/i],
|
|
152
|
+
warning: [/warning/i, /yellow/i],
|
|
153
|
+
error: [/error/i, /danger/i, /red/i],
|
|
154
|
+
info: [/info/i, /blue/i]
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
Tabs: {
|
|
158
|
+
selectors: [
|
|
159
|
+
'[class*="tabs"]',
|
|
160
|
+
'[role="tablist"]',
|
|
161
|
+
'[class*="tab-list"]'
|
|
162
|
+
],
|
|
163
|
+
variants: {
|
|
164
|
+
default: [/tabs(?!-)/i]
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
Navigation: {
|
|
168
|
+
selectors: [
|
|
169
|
+
'nav',
|
|
170
|
+
'[class*="nav"]',
|
|
171
|
+
'[class*="sidebar"]',
|
|
172
|
+
'[class*="menu"]',
|
|
173
|
+
'[role="navigation"]'
|
|
174
|
+
],
|
|
175
|
+
variants: {
|
|
176
|
+
horizontal: [/horizontal/i, /navbar/i, /topnav/i],
|
|
177
|
+
vertical: [/vertical/i, /sidebar/i, /sidenav/i]
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
class ComponentExtractor {
|
|
183
|
+
constructor(options = {}) {
|
|
184
|
+
this.options = {
|
|
185
|
+
minOccurrences: 1,
|
|
186
|
+
generateTypeScript: true,
|
|
187
|
+
...options
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
this.components = new Map();
|
|
191
|
+
this.registry = {
|
|
192
|
+
version: '1.0.0',
|
|
193
|
+
generatedAt: new Date().toISOString(),
|
|
194
|
+
components: {}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Load and analyze an HTML file
|
|
200
|
+
*/
|
|
201
|
+
analyzeFile(htmlPath) {
|
|
202
|
+
if (!fs.existsSync(htmlPath)) {
|
|
203
|
+
throw new Error(`HTML file not found: ${htmlPath}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const html = fs.readFileSync(htmlPath, 'utf-8');
|
|
207
|
+
const dom = new JSDOM(html);
|
|
208
|
+
const document = dom.window.document;
|
|
209
|
+
const fileName = path.basename(htmlPath, '.html');
|
|
210
|
+
|
|
211
|
+
for (const [componentType, config] of Object.entries(COMPONENT_PATTERNS)) {
|
|
212
|
+
this.extractComponent(document, componentType, config, fileName);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Analyze multiple HTML files in a directory
|
|
220
|
+
*/
|
|
221
|
+
analyzeDirectory(htmlDir) {
|
|
222
|
+
if (!fs.existsSync(htmlDir)) {
|
|
223
|
+
throw new Error(`Directory not found: ${htmlDir}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const files = fs.readdirSync(htmlDir)
|
|
227
|
+
.filter(f => f.endsWith('.html'));
|
|
228
|
+
|
|
229
|
+
for (const file of files) {
|
|
230
|
+
this.analyzeFile(path.join(htmlDir, file));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Extract a specific component type from document
|
|
238
|
+
*/
|
|
239
|
+
extractComponent(document, componentType, config, sourceFile) {
|
|
240
|
+
for (const selector of config.selectors) {
|
|
241
|
+
try {
|
|
242
|
+
const elements = document.querySelectorAll(selector);
|
|
243
|
+
|
|
244
|
+
for (const element of elements) {
|
|
245
|
+
// Skip if matches exclude selector
|
|
246
|
+
if (config.excludeSelectors) {
|
|
247
|
+
const shouldExclude = config.excludeSelectors.some(excl => {
|
|
248
|
+
try {
|
|
249
|
+
return element.matches(excl);
|
|
250
|
+
} catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
if (shouldExclude) continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Determine variant
|
|
258
|
+
const variant = this.detectVariant(element, config.variants);
|
|
259
|
+
|
|
260
|
+
// Extract component data
|
|
261
|
+
const componentData = {
|
|
262
|
+
type: componentType,
|
|
263
|
+
variant,
|
|
264
|
+
className: element.className || '',
|
|
265
|
+
tagName: element.tagName.toLowerCase(),
|
|
266
|
+
html: element.outerHTML.substring(0, 500), // Limit HTML size
|
|
267
|
+
styles: this.extractStyles(element),
|
|
268
|
+
attributes: this.extractAttributes(element),
|
|
269
|
+
sourceFile,
|
|
270
|
+
selector
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Add to components map
|
|
274
|
+
const key = `${componentType}-${variant}`;
|
|
275
|
+
if (!this.components.has(key)) {
|
|
276
|
+
this.components.set(key, []);
|
|
277
|
+
}
|
|
278
|
+
this.components.get(key).push(componentData);
|
|
279
|
+
}
|
|
280
|
+
} catch (e) {
|
|
281
|
+
// Invalid selector, skip
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Detect variant from element classes
|
|
288
|
+
*/
|
|
289
|
+
detectVariant(element, variants) {
|
|
290
|
+
const className = element.className || '';
|
|
291
|
+
|
|
292
|
+
for (const [variant, patterns] of Object.entries(variants)) {
|
|
293
|
+
for (const pattern of patterns) {
|
|
294
|
+
if (pattern.test(className)) {
|
|
295
|
+
return variant;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return 'default';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Extract inline styles
|
|
305
|
+
*/
|
|
306
|
+
extractStyles(element) {
|
|
307
|
+
const styleAttr = element.getAttribute('style');
|
|
308
|
+
if (!styleAttr) return {};
|
|
309
|
+
|
|
310
|
+
const styles = {};
|
|
311
|
+
const rules = styleAttr.split(';').filter(Boolean);
|
|
312
|
+
|
|
313
|
+
for (const rule of rules) {
|
|
314
|
+
const [prop, value] = rule.split(':').map(s => s.trim());
|
|
315
|
+
if (prop && value) {
|
|
316
|
+
const camelProp = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
317
|
+
styles[camelProp] = value;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return styles;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Extract relevant attributes
|
|
326
|
+
*/
|
|
327
|
+
extractAttributes(element) {
|
|
328
|
+
const attrs = {};
|
|
329
|
+
const relevantAttrs = ['type', 'role', 'aria-label', 'placeholder', 'disabled'];
|
|
330
|
+
|
|
331
|
+
for (const attr of relevantAttrs) {
|
|
332
|
+
const value = element.getAttribute(attr);
|
|
333
|
+
if (value !== null) {
|
|
334
|
+
attrs[attr] = value;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return attrs;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Generate React component file
|
|
343
|
+
*/
|
|
344
|
+
generateComponent(componentType, variants) {
|
|
345
|
+
const ext = this.options.generateTypeScript ? 'tsx' : 'jsx';
|
|
346
|
+
const propsType = this.options.generateTypeScript ? 'Props' : '';
|
|
347
|
+
|
|
348
|
+
// Get unique classes across all variants
|
|
349
|
+
const allClasses = new Set();
|
|
350
|
+
const variantClasses = {};
|
|
351
|
+
|
|
352
|
+
for (const [key, instances] of this.components.entries()) {
|
|
353
|
+
if (!key.startsWith(componentType)) continue;
|
|
354
|
+
|
|
355
|
+
const variant = key.split('-').slice(1).join('-');
|
|
356
|
+
variantClasses[variant] = new Set();
|
|
357
|
+
|
|
358
|
+
for (const instance of instances) {
|
|
359
|
+
const classes = instance.className.split(/\s+/).filter(Boolean);
|
|
360
|
+
classes.forEach(c => {
|
|
361
|
+
allClasses.add(c);
|
|
362
|
+
variantClasses[variant].add(c);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Get most common base classes
|
|
368
|
+
const baseClasses = this.getMostCommonClasses(componentType);
|
|
369
|
+
|
|
370
|
+
// Generate component
|
|
371
|
+
const variantNames = Object.keys(variantClasses).filter(v => variantClasses[v].size > 0);
|
|
372
|
+
|
|
373
|
+
let component = '';
|
|
374
|
+
|
|
375
|
+
if (this.options.generateTypeScript) {
|
|
376
|
+
component += `import React from 'react';\n\n`;
|
|
377
|
+
|
|
378
|
+
// Generate props interface
|
|
379
|
+
component += `interface ${componentType}Props {\n`;
|
|
380
|
+
component += ` variant?: ${variantNames.map(v => `'${v}'`).join(' | ') || "'default'"};\n`;
|
|
381
|
+
component += ` className?: string;\n`;
|
|
382
|
+
component += ` children?: React.ReactNode;\n`;
|
|
383
|
+
|
|
384
|
+
// Add specific props based on component type
|
|
385
|
+
if (componentType === 'Button') {
|
|
386
|
+
component += ` onClick?: () => void;\n`;
|
|
387
|
+
component += ` disabled?: boolean;\n`;
|
|
388
|
+
component += ` type?: 'button' | 'submit' | 'reset';\n`;
|
|
389
|
+
} else if (componentType === 'Input') {
|
|
390
|
+
component += ` value?: string;\n`;
|
|
391
|
+
component += ` onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;\n`;
|
|
392
|
+
component += ` placeholder?: string;\n`;
|
|
393
|
+
component += ` disabled?: boolean;\n`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
component += `}\n\n`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Generate variant classes map
|
|
400
|
+
component += `const variantClasses = {\n`;
|
|
401
|
+
for (const [variant, classes] of Object.entries(variantClasses)) {
|
|
402
|
+
const classArray = Array.from(classes).slice(0, 10);
|
|
403
|
+
component += ` ${variant}: '${classArray.join(' ')}',\n`;
|
|
404
|
+
}
|
|
405
|
+
component += `};\n\n`;
|
|
406
|
+
|
|
407
|
+
// Generate component function
|
|
408
|
+
const funcDef = this.options.generateTypeScript
|
|
409
|
+
? `export const ${componentType}: React.FC<${componentType}Props>`
|
|
410
|
+
: `export const ${componentType}`;
|
|
411
|
+
|
|
412
|
+
component += `${funcDef} = ({\n`;
|
|
413
|
+
component += ` variant = 'default',\n`;
|
|
414
|
+
component += ` className = '',\n`;
|
|
415
|
+
component += ` children,\n`;
|
|
416
|
+
|
|
417
|
+
if (componentType === 'Button') {
|
|
418
|
+
component += ` onClick,\n`;
|
|
419
|
+
component += ` disabled = false,\n`;
|
|
420
|
+
component += ` type = 'button',\n`;
|
|
421
|
+
} else if (componentType === 'Input') {
|
|
422
|
+
component += ` value,\n`;
|
|
423
|
+
component += ` onChange,\n`;
|
|
424
|
+
component += ` placeholder,\n`;
|
|
425
|
+
component += ` disabled = false,\n`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
component += ` ...props\n`;
|
|
429
|
+
component += `}) => {\n`;
|
|
430
|
+
component += ` const baseClasses = '${baseClasses.join(' ')}';\n`;
|
|
431
|
+
component += ` const variantClass = variantClasses[variant] || variantClasses.default;\n`;
|
|
432
|
+
component += ` const combinedClasses = \`\${baseClasses} \${variantClass} \${className}\`.trim();\n\n`;
|
|
433
|
+
|
|
434
|
+
// Generate JSX based on component type
|
|
435
|
+
if (componentType === 'Button') {
|
|
436
|
+
component += ` return (\n`;
|
|
437
|
+
component += ` <button\n`;
|
|
438
|
+
component += ` type={type}\n`;
|
|
439
|
+
component += ` className={combinedClasses}\n`;
|
|
440
|
+
component += ` onClick={onClick}\n`;
|
|
441
|
+
component += ` disabled={disabled}\n`;
|
|
442
|
+
component += ` {...props}\n`;
|
|
443
|
+
component += ` >\n`;
|
|
444
|
+
component += ` {children}\n`;
|
|
445
|
+
component += ` </button>\n`;
|
|
446
|
+
component += ` );\n`;
|
|
447
|
+
} else if (componentType === 'Input') {
|
|
448
|
+
component += ` return (\n`;
|
|
449
|
+
component += ` <input\n`;
|
|
450
|
+
component += ` className={combinedClasses}\n`;
|
|
451
|
+
component += ` value={value}\n`;
|
|
452
|
+
component += ` onChange={onChange}\n`;
|
|
453
|
+
component += ` placeholder={placeholder}\n`;
|
|
454
|
+
component += ` disabled={disabled}\n`;
|
|
455
|
+
component += ` {...props}\n`;
|
|
456
|
+
component += ` />\n`;
|
|
457
|
+
component += ` );\n`;
|
|
458
|
+
} else if (componentType === 'Card') {
|
|
459
|
+
component += ` return (\n`;
|
|
460
|
+
component += ` <div className={combinedClasses} {...props}>\n`;
|
|
461
|
+
component += ` {children}\n`;
|
|
462
|
+
component += ` </div>\n`;
|
|
463
|
+
component += ` );\n`;
|
|
464
|
+
} else {
|
|
465
|
+
component += ` return (\n`;
|
|
466
|
+
component += ` <div className={combinedClasses} {...props}>\n`;
|
|
467
|
+
component += ` {children}\n`;
|
|
468
|
+
component += ` </div>\n`;
|
|
469
|
+
component += ` );\n`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
component += `};\n\n`;
|
|
473
|
+
component += `export default ${componentType};\n`;
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
name: componentType,
|
|
477
|
+
filename: `${componentType}.${ext}`,
|
|
478
|
+
content: component,
|
|
479
|
+
variants: variantNames
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Get most common classes for a component type
|
|
485
|
+
*/
|
|
486
|
+
getMostCommonClasses(componentType) {
|
|
487
|
+
const classCounts = new Map();
|
|
488
|
+
|
|
489
|
+
for (const [key, instances] of this.components.entries()) {
|
|
490
|
+
if (!key.startsWith(componentType)) continue;
|
|
491
|
+
|
|
492
|
+
for (const instance of instances) {
|
|
493
|
+
const classes = instance.className.split(/\s+/).filter(Boolean);
|
|
494
|
+
for (const cls of classes) {
|
|
495
|
+
classCounts.set(cls, (classCounts.get(cls) || 0) + 1);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Get classes that appear in most instances
|
|
501
|
+
const sorted = Array.from(classCounts.entries())
|
|
502
|
+
.sort((a, b) => b[1] - a[1])
|
|
503
|
+
.slice(0, 5)
|
|
504
|
+
.map(([cls]) => cls);
|
|
505
|
+
|
|
506
|
+
return sorted;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Generate all components
|
|
511
|
+
*/
|
|
512
|
+
generateAll() {
|
|
513
|
+
const generated = [];
|
|
514
|
+
const componentTypes = new Set();
|
|
515
|
+
|
|
516
|
+
// Get unique component types
|
|
517
|
+
for (const key of this.components.keys()) {
|
|
518
|
+
const type = key.split('-')[0];
|
|
519
|
+
componentTypes.add(type);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Generate each component
|
|
523
|
+
for (const type of componentTypes) {
|
|
524
|
+
const variants = {};
|
|
525
|
+
for (const [key, instances] of this.components.entries()) {
|
|
526
|
+
if (key.startsWith(type)) {
|
|
527
|
+
const variant = key.split('-').slice(1).join('-');
|
|
528
|
+
variants[variant] = instances;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (Object.keys(variants).length > 0) {
|
|
533
|
+
const component = this.generateComponent(type, variants);
|
|
534
|
+
generated.push(component);
|
|
535
|
+
|
|
536
|
+
// Add to registry
|
|
537
|
+
this.registry.components[type] = {
|
|
538
|
+
path: `components/extracted/${component.filename}`,
|
|
539
|
+
variants: component.variants,
|
|
540
|
+
instances: Object.values(variants).flat().length
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return generated;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Write components to output directory
|
|
550
|
+
*/
|
|
551
|
+
writeComponents(outputDir) {
|
|
552
|
+
if (!fs.existsSync(outputDir)) {
|
|
553
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const components = this.generateAll();
|
|
557
|
+
const written = [];
|
|
558
|
+
|
|
559
|
+
for (const component of components) {
|
|
560
|
+
const filePath = path.join(outputDir, component.filename);
|
|
561
|
+
fs.writeFileSync(filePath, component.content);
|
|
562
|
+
written.push(filePath);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Write registry
|
|
566
|
+
const registryPath = path.join(outputDir, 'registry.json');
|
|
567
|
+
fs.writeFileSync(registryPath, JSON.stringify(this.registry, null, 2));
|
|
568
|
+
written.push(registryPath);
|
|
569
|
+
|
|
570
|
+
// Write index file
|
|
571
|
+
const indexContent = this.generateIndex(components);
|
|
572
|
+
const indexPath = path.join(outputDir, `index.${this.options.generateTypeScript ? 'ts' : 'js'}`);
|
|
573
|
+
fs.writeFileSync(indexPath, indexContent);
|
|
574
|
+
written.push(indexPath);
|
|
575
|
+
|
|
576
|
+
return written;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Generate index file
|
|
581
|
+
*/
|
|
582
|
+
generateIndex(components) {
|
|
583
|
+
let content = '// Auto-generated component library index\n\n';
|
|
584
|
+
|
|
585
|
+
for (const component of components) {
|
|
586
|
+
const name = component.name;
|
|
587
|
+
content += `export { ${name} } from './${name}';\n`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return content;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Get extraction summary
|
|
595
|
+
*/
|
|
596
|
+
getSummary() {
|
|
597
|
+
const summary = {
|
|
598
|
+
totalComponents: this.components.size,
|
|
599
|
+
byType: {}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
for (const [key, instances] of this.components.entries()) {
|
|
603
|
+
const type = key.split('-')[0];
|
|
604
|
+
if (!summary.byType[type]) {
|
|
605
|
+
summary.byType[type] = { variants: [], instances: 0 };
|
|
606
|
+
}
|
|
607
|
+
const variant = key.split('-').slice(1).join('-');
|
|
608
|
+
if (!summary.byType[type].variants.includes(variant)) {
|
|
609
|
+
summary.byType[type].variants.push(variant);
|
|
610
|
+
}
|
|
611
|
+
summary.byType[type].instances += instances.length;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return summary;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Format summary for CLI output
|
|
619
|
+
*/
|
|
620
|
+
formatSummary() {
|
|
621
|
+
const summary = this.getSummary();
|
|
622
|
+
const lines = [];
|
|
623
|
+
|
|
624
|
+
lines.push('\x1b[1mComponent Library Extraction Summary\x1b[0m\n');
|
|
625
|
+
lines.push(`Total component types found: ${Object.keys(summary.byType).length}\n`);
|
|
626
|
+
|
|
627
|
+
for (const [type, data] of Object.entries(summary.byType)) {
|
|
628
|
+
lines.push(`\x1b[1m${type}\x1b[0m`);
|
|
629
|
+
lines.push(` Variants: ${data.variants.join(', ')}`);
|
|
630
|
+
lines.push(` Instances: ${data.instances}`);
|
|
631
|
+
lines.push('');
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return lines.join('\n');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Extract components from HTML directory
|
|
640
|
+
*/
|
|
641
|
+
function extractComponents(htmlDir, options = {}) {
|
|
642
|
+
const extractor = new ComponentExtractor(options);
|
|
643
|
+
extractor.analyzeDirectory(htmlDir);
|
|
644
|
+
return extractor;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// CLI execution
|
|
648
|
+
if (require.main === module) {
|
|
649
|
+
const args = process.argv.slice(2);
|
|
650
|
+
let inputDir = null;
|
|
651
|
+
let outputDir = null;
|
|
652
|
+
let projectName = null;
|
|
653
|
+
|
|
654
|
+
for (let i = 0; i < args.length; i++) {
|
|
655
|
+
switch (args[i]) {
|
|
656
|
+
case '--input':
|
|
657
|
+
case '-i':
|
|
658
|
+
inputDir = args[++i];
|
|
659
|
+
break;
|
|
660
|
+
case '--output':
|
|
661
|
+
case '-o':
|
|
662
|
+
outputDir = args[++i];
|
|
663
|
+
break;
|
|
664
|
+
case '--project':
|
|
665
|
+
projectName = args[++i];
|
|
666
|
+
break;
|
|
667
|
+
case '--help':
|
|
668
|
+
case '-h':
|
|
669
|
+
console.log(`
|
|
670
|
+
Usage: node extract-components.js [options]
|
|
671
|
+
|
|
672
|
+
Options:
|
|
673
|
+
--input, -i <path> Input directory containing HTML files
|
|
674
|
+
--output, -o <path> Output directory for generated components
|
|
675
|
+
--project <name> Project name
|
|
676
|
+
--help, -h Show this help
|
|
677
|
+
|
|
678
|
+
Examples:
|
|
679
|
+
node extract-components.js --project my-app
|
|
680
|
+
node extract-components.js -i ./html -o ./components/extracted
|
|
681
|
+
`);
|
|
682
|
+
process.exit(0);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Handle project-based paths
|
|
687
|
+
if (projectName) {
|
|
688
|
+
const SKILL_DIR = path.dirname(__dirname);
|
|
689
|
+
const PROJECTS_DIR = path.resolve(SKILL_DIR, '../../../projects');
|
|
690
|
+
const projectDir = path.join(PROJECTS_DIR, projectName);
|
|
691
|
+
|
|
692
|
+
inputDir = inputDir || path.join(projectDir, 'references', 'html');
|
|
693
|
+
outputDir = outputDir || path.join(projectDir, 'prototype', 'src', 'components', 'extracted');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (!inputDir) {
|
|
697
|
+
console.error('\x1b[31mError:\x1b[0m --input or --project is required');
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
console.log(`\n\x1b[1mComponent Library Extractor\x1b[0m`);
|
|
703
|
+
console.log(`Input: ${inputDir}`);
|
|
704
|
+
|
|
705
|
+
const extractor = extractComponents(inputDir);
|
|
706
|
+
|
|
707
|
+
console.log('');
|
|
708
|
+
console.log(extractor.formatSummary());
|
|
709
|
+
|
|
710
|
+
if (outputDir) {
|
|
711
|
+
console.log(`\x1b[1mWriting to:\x1b[0m ${outputDir}`);
|
|
712
|
+
const written = extractor.writeComponents(outputDir);
|
|
713
|
+
console.log(`\n\x1b[32m✓ Wrote ${written.length} files:\x1b[0m`);
|
|
714
|
+
for (const file of written) {
|
|
715
|
+
console.log(` ${path.basename(file)}`);
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
console.log('\x1b[33mTip:\x1b[0m Use --output <dir> to generate component files');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
} catch (error) {
|
|
722
|
+
console.error(`\x1b[31mError:\x1b[0m ${error.message}`);
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
module.exports = {
|
|
728
|
+
ComponentExtractor,
|
|
729
|
+
extractComponents,
|
|
730
|
+
COMPONENT_PATTERNS
|
|
731
|
+
};
|