skillui 1.1.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/dist/cli.d.ts +3 -0
- package/dist/cli.js +202 -0
- package/dist/extractors/components.d.ts +11 -0
- package/dist/extractors/components.js +455 -0
- package/dist/extractors/framework.d.ts +4 -0
- package/dist/extractors/framework.js +126 -0
- package/dist/extractors/tokens/computed.d.ts +7 -0
- package/dist/extractors/tokens/computed.js +249 -0
- package/dist/extractors/tokens/css.d.ts +3 -0
- package/dist/extractors/tokens/css.js +510 -0
- package/dist/extractors/tokens/http-css.d.ts +14 -0
- package/dist/extractors/tokens/http-css.js +1689 -0
- package/dist/extractors/tokens/tailwind.d.ts +3 -0
- package/dist/extractors/tokens/tailwind.js +353 -0
- package/dist/extractors/tokens/tokens-file.d.ts +3 -0
- package/dist/extractors/tokens/tokens-file.js +229 -0
- package/dist/extractors/ultra/animations.d.ts +21 -0
- package/dist/extractors/ultra/animations.js +530 -0
- package/dist/extractors/ultra/components-dom.d.ts +13 -0
- package/dist/extractors/ultra/components-dom.js +152 -0
- package/dist/extractors/ultra/interactions.d.ts +14 -0
- package/dist/extractors/ultra/interactions.js +225 -0
- package/dist/extractors/ultra/layout.d.ts +14 -0
- package/dist/extractors/ultra/layout.js +126 -0
- package/dist/extractors/ultra/pages.d.ts +16 -0
- package/dist/extractors/ultra/pages.js +231 -0
- package/dist/font-resolver.d.ts +10 -0
- package/dist/font-resolver.js +280 -0
- package/dist/modes/dir.d.ts +6 -0
- package/dist/modes/dir.js +213 -0
- package/dist/modes/repo.d.ts +6 -0
- package/dist/modes/repo.js +76 -0
- package/dist/modes/ultra.d.ts +22 -0
- package/dist/modes/ultra.js +285 -0
- package/dist/modes/url.d.ts +14 -0
- package/dist/modes/url.js +161 -0
- package/dist/normalizer.d.ts +11 -0
- package/dist/normalizer.js +867 -0
- package/dist/screenshot.d.ts +9 -0
- package/dist/screenshot.js +94 -0
- package/dist/types-ultra.d.ts +157 -0
- package/dist/types-ultra.js +4 -0
- package/dist/types.d.ts +182 -0
- package/dist/types.js +4 -0
- package/dist/writers/animations-md.d.ts +17 -0
- package/dist/writers/animations-md.js +313 -0
- package/dist/writers/components-md.d.ts +8 -0
- package/dist/writers/components-md.js +151 -0
- package/dist/writers/design-md.d.ts +7 -0
- package/dist/writers/design-md.js +704 -0
- package/dist/writers/interactions-md.d.ts +8 -0
- package/dist/writers/interactions-md.js +146 -0
- package/dist/writers/layout-md.d.ts +8 -0
- package/dist/writers/layout-md.js +120 -0
- package/dist/writers/skill.d.ts +12 -0
- package/dist/writers/skill.js +1006 -0
- package/dist/writers/tokens-json.d.ts +11 -0
- package/dist/writers/tokens-json.js +164 -0
- package/package.json +78 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const commander_1 = require("commander");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const dir_1 = require("./modes/dir");
|
|
41
|
+
const repo_1 = require("./modes/repo");
|
|
42
|
+
const url_1 = require("./modes/url");
|
|
43
|
+
const ultra_1 = require("./modes/ultra");
|
|
44
|
+
const design_md_1 = require("./writers/design-md");
|
|
45
|
+
const skill_1 = require("./writers/skill");
|
|
46
|
+
const VERSION = '1.1.0';
|
|
47
|
+
const program = new commander_1.Command();
|
|
48
|
+
program
|
|
49
|
+
.name('skillui')
|
|
50
|
+
.description('Reverse-engineer design systems from any project. Pure static analysis — no AI, no API keys.')
|
|
51
|
+
.version(VERSION)
|
|
52
|
+
.option('--dir <path>', 'Scan a local project directory')
|
|
53
|
+
.option('--repo <url>', 'Clone and scan a git repository')
|
|
54
|
+
.option('--url <url>', 'Crawl a live website')
|
|
55
|
+
.option('--out <path>', 'Output directory', './')
|
|
56
|
+
.option('--name <string>', 'Override project name')
|
|
57
|
+
.option('--no-skill', 'Output DESIGN.md only, skip .skill packaging')
|
|
58
|
+
.option('--format <format>', 'Output format: design-md | skill | both', 'both')
|
|
59
|
+
.option('--mode <mode>', 'Extraction mode: default | ultra (ultra adds multi-page screenshots, interactions, layout, DOM components)', 'default')
|
|
60
|
+
.option('--screens <number>', 'Ultra mode: max pages to crawl for screenshots (default: 5)', '5')
|
|
61
|
+
.action(async (opts) => {
|
|
62
|
+
printBanner(opts.mode);
|
|
63
|
+
// Validate: exactly one mode must be specified
|
|
64
|
+
const modes = [opts.dir, opts.repo, opts.url].filter(Boolean);
|
|
65
|
+
if (modes.length === 0) {
|
|
66
|
+
console.error('\n Error: Specify one of --dir, --repo, or --url\n');
|
|
67
|
+
program.help();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (modes.length > 1) {
|
|
71
|
+
console.error('\n Error: Specify only one of --dir, --repo, or --url\n');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
let profile;
|
|
76
|
+
let screenshotPath = null;
|
|
77
|
+
// ── Write outputs dir first (needed for screenshot path in URL mode) ──
|
|
78
|
+
const outputDir = path.resolve(opts.out);
|
|
79
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
80
|
+
// ── Run the selected mode ────────────────────────────────
|
|
81
|
+
if (opts.dir) {
|
|
82
|
+
const resolvedDir = path.resolve(opts.dir);
|
|
83
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
84
|
+
console.error(`\n Error: Directory not found: ${resolvedDir}\n`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
console.log(` Mode: dir \u2192 ${resolvedDir}`);
|
|
88
|
+
profile = await (0, dir_1.runDirMode)(resolvedDir, opts.name);
|
|
89
|
+
}
|
|
90
|
+
else if (opts.repo) {
|
|
91
|
+
console.log(` Mode: repo \u2192 ${opts.repo}`);
|
|
92
|
+
profile = await (0, repo_1.runRepoMode)(opts.repo, opts.name);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log(` Mode: url \u2192 ${opts.url}`);
|
|
96
|
+
// Compute skillDir early so screenshot can be saved there
|
|
97
|
+
const safeName = (opts.name || new URL(opts.url).hostname.replace(/^www\./, '').split('.')[0])
|
|
98
|
+
.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
|
|
99
|
+
const skillDir = path.join(outputDir, `${safeName}-design`);
|
|
100
|
+
fs.mkdirSync(path.join(skillDir, 'screenshots'), { recursive: true });
|
|
101
|
+
const urlResult = await (0, url_1.runUrlMode)(opts.url, opts.name, skillDir);
|
|
102
|
+
profile = urlResult.profile;
|
|
103
|
+
screenshotPath = urlResult.screenshotPath;
|
|
104
|
+
}
|
|
105
|
+
// ── Ultra mode (URL only) ────────────────────────────────
|
|
106
|
+
const isUltra = opts.mode === 'ultra' && !!opts.url;
|
|
107
|
+
let ultraAnimations = null;
|
|
108
|
+
if (isUltra) {
|
|
109
|
+
const ultraScreens = Math.max(1, Math.min(20, parseInt(opts.screens, 10) || 5));
|
|
110
|
+
const safeName = (opts.name || new URL(opts.url).hostname.replace(/^www\./, '').split('.')[0])
|
|
111
|
+
.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
|
|
112
|
+
const skillDir = path.join(outputDir, `${safeName}-design`);
|
|
113
|
+
const ultraResult = await (0, ultra_1.runUltraMode)(opts.url, profile, skillDir, { screens: ultraScreens });
|
|
114
|
+
ultraAnimations = ultraResult.animations;
|
|
115
|
+
}
|
|
116
|
+
// ── Print extraction summary ─────────────────────────────
|
|
117
|
+
printSummary(profile);
|
|
118
|
+
const shouldWriteDesignMd = opts.format === 'design-md' || opts.format === 'both';
|
|
119
|
+
const shouldWriteSkill = opts.skill !== false && (opts.format === 'skill' || opts.format === 'both');
|
|
120
|
+
const designMdContent = (0, design_md_1.generateDesignMd)(profile, screenshotPath);
|
|
121
|
+
// Write DESIGN.md
|
|
122
|
+
if (shouldWriteDesignMd) {
|
|
123
|
+
const safeName = profile.projectName.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
|
|
124
|
+
const designDir = path.join(outputDir, `${safeName}-design`);
|
|
125
|
+
fs.mkdirSync(designDir, { recursive: true });
|
|
126
|
+
const designMdPath = path.join(designDir, 'DESIGN.md');
|
|
127
|
+
fs.writeFileSync(designMdPath, designMdContent, 'utf-8');
|
|
128
|
+
process.stdout.write(' Writing DESIGN.md...');
|
|
129
|
+
console.log(' \u2713');
|
|
130
|
+
}
|
|
131
|
+
// Write .skill
|
|
132
|
+
let skillFilePath = '';
|
|
133
|
+
if (shouldWriteSkill) {
|
|
134
|
+
process.stdout.write(' Bundling fonts & packaging .skill...');
|
|
135
|
+
const result = await (0, skill_1.generateSkill)(profile, designMdContent, outputDir, screenshotPath, ultraAnimations);
|
|
136
|
+
skillFilePath = result.skillFile;
|
|
137
|
+
console.log(' \u2713');
|
|
138
|
+
}
|
|
139
|
+
// ── Print output paths ───────────────────────────────────
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(' Output:');
|
|
142
|
+
if (shouldWriteDesignMd) {
|
|
143
|
+
const safeName = profile.projectName.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
|
|
144
|
+
const relPath = path.relative(process.cwd(), path.join(outputDir, `${safeName}-design`, 'DESIGN.md'));
|
|
145
|
+
console.log(` ./${relPath.replace(/\\/g, '/')}`);
|
|
146
|
+
}
|
|
147
|
+
if (shouldWriteSkill && skillFilePath) {
|
|
148
|
+
const relPath = path.relative(process.cwd(), skillFilePath);
|
|
149
|
+
console.log(` ./${relPath.replace(/\\/g, '/')}`);
|
|
150
|
+
}
|
|
151
|
+
if (shouldWriteSkill && skillFilePath) {
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(' Install skill:');
|
|
154
|
+
const relPath = path.relative(process.cwd(), skillFilePath);
|
|
155
|
+
console.log(` claude skill install ./${relPath.replace(/\\/g, '/')}`);
|
|
156
|
+
}
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log(' Or just drop DESIGN.md into your project root.');
|
|
159
|
+
console.log('');
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.error(`\n Error: ${err.message || err}\n`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
program.parse();
|
|
167
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
168
|
+
function printBanner(mode) {
|
|
169
|
+
console.log('');
|
|
170
|
+
const modeLabel = mode === 'ultra' ? ` (ultra mode)` : '';
|
|
171
|
+
console.log(` skillui v${VERSION}${modeLabel}`);
|
|
172
|
+
console.log('');
|
|
173
|
+
}
|
|
174
|
+
function printSummary(profile) {
|
|
175
|
+
const frameworks = profile.frameworks.map(f => f.name).join(' + ') || 'No framework detected';
|
|
176
|
+
const colorCount = profile.colors.length;
|
|
177
|
+
const fontCount = new Set(profile.typography.map(t => t.fontFamily)).size;
|
|
178
|
+
const gridBase = profile.spacing.base;
|
|
179
|
+
const componentCount = profile.components.length;
|
|
180
|
+
const animCount = profile.animations.length;
|
|
181
|
+
const darkMode = profile.designTraits.hasDarkMode ? 'yes' : 'no';
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(` Detected: ${frameworks}`);
|
|
184
|
+
console.log(` Extracted: ${colorCount} colors \u00B7 ${fontCount} fonts \u00B7 ${gridBase}px grid \u00B7 ${componentCount} components`);
|
|
185
|
+
if (profile.iconLibrary || profile.stateLibrary) {
|
|
186
|
+
const libs = [];
|
|
187
|
+
if (profile.iconLibrary)
|
|
188
|
+
libs.push(`icons: ${profile.iconLibrary}`);
|
|
189
|
+
if (profile.stateLibrary)
|
|
190
|
+
libs.push(`state: ${profile.stateLibrary}`);
|
|
191
|
+
console.log(` Libraries: ${libs.join(' \u00B7 ')}`);
|
|
192
|
+
}
|
|
193
|
+
if (animCount > 0 || profile.designTraits.hasDarkMode) {
|
|
194
|
+
const extras = [];
|
|
195
|
+
if (animCount > 0)
|
|
196
|
+
extras.push(`${animCount} animations`);
|
|
197
|
+
extras.push(`dark mode: ${darkMode}`);
|
|
198
|
+
console.log(` Features: ${extras.join(' \u00B7 ')}`);
|
|
199
|
+
}
|
|
200
|
+
console.log('');
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ComponentInfo } from '../types';
|
|
2
|
+
export declare function extractComponents(projectDir: string): ComponentInfo[];
|
|
3
|
+
/**
|
|
4
|
+
* Detect which libraries the project uses (icons, state, animation).
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectProjectLibraries(projectDir: string): {
|
|
7
|
+
iconLibrary: string | null;
|
|
8
|
+
stateLibrary: string | null;
|
|
9
|
+
animationLibrary: string | null;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=components.d.ts.map
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.extractComponents = extractComponents;
|
|
37
|
+
exports.detectProjectLibraries = detectProjectLibraries;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const COMPONENT_DIRS = [
|
|
41
|
+
'src/components',
|
|
42
|
+
'components',
|
|
43
|
+
'app/components',
|
|
44
|
+
'src/app/components',
|
|
45
|
+
'src/ui',
|
|
46
|
+
'src/common',
|
|
47
|
+
'lib/components',
|
|
48
|
+
'src/lib/components',
|
|
49
|
+
'src/features',
|
|
50
|
+
'src/views',
|
|
51
|
+
'src/pages',
|
|
52
|
+
'app',
|
|
53
|
+
'pages',
|
|
54
|
+
];
|
|
55
|
+
const COMPONENT_EXTENSIONS = ['.tsx', '.jsx', '.vue', '.svelte'];
|
|
56
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '__tests__', '__mocks__', '.next', '.nuxt'];
|
|
57
|
+
// Category detection keywords for component names and file paths
|
|
58
|
+
const CATEGORY_PATTERNS = {
|
|
59
|
+
'layout': /^(layout|container|grid|stack|flex|page|section|wrapper|sidebar|header|footer|main|shell|frame|panel|divider|spacer|center)/i,
|
|
60
|
+
'navigation': /^(nav|navbar|menu|breadcrumb|tab|tabs|pagination|stepper|link|drawer|appbar|topbar|bottombar)/i,
|
|
61
|
+
'data-display': /^(card|list|table|avatar|badge|chip|tag|stat|metric|tooltip|accordion|collapse|timeline|tree|calendar|chart|graph|progress|indicator)/i,
|
|
62
|
+
'data-input': /^(input|form|select|checkbox|radio|switch|toggle|slider|textarea|datepicker|timepicker|upload|dropdown|combobox|autocomplete|search|filter|rating)/i,
|
|
63
|
+
'feedback': /^(alert|toast|snackbar|notification|banner|skeleton|spinner|loading|error|empty|placeholder|progress)/i,
|
|
64
|
+
'overlay': /^(modal|dialog|popover|sheet|bottomsheet|lightbox|overlay|confirm|command|commandpalette)/i,
|
|
65
|
+
'typography': /^(heading|text|title|paragraph|label|caption|code|highlight|prose|markdown|typography)/i,
|
|
66
|
+
'media': /^(image|video|icon|logo|thumbnail|gallery|carousel|slider|embed|player)/i,
|
|
67
|
+
'other': /./,
|
|
68
|
+
};
|
|
69
|
+
function extractComponents(projectDir) {
|
|
70
|
+
const components = [];
|
|
71
|
+
for (const dir of COMPONENT_DIRS) {
|
|
72
|
+
const fullDir = path.join(projectDir, dir);
|
|
73
|
+
if (fs.existsSync(fullDir)) {
|
|
74
|
+
scanDirectory(fullDir, components, projectDir);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return components;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Detect which libraries the project uses (icons, state, animation).
|
|
81
|
+
*/
|
|
82
|
+
function detectProjectLibraries(projectDir) {
|
|
83
|
+
let iconLibrary = null;
|
|
84
|
+
let stateLibrary = null;
|
|
85
|
+
let animationLibrary = null;
|
|
86
|
+
try {
|
|
87
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
88
|
+
if (fs.existsSync(pkgPath)) {
|
|
89
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
90
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
91
|
+
// Icon libraries
|
|
92
|
+
if (allDeps['lucide-react'] || allDeps['lucide-vue-next'])
|
|
93
|
+
iconLibrary = 'Lucide';
|
|
94
|
+
else if (allDeps['@heroicons/react'] || allDeps['heroicons'])
|
|
95
|
+
iconLibrary = 'Heroicons';
|
|
96
|
+
else if (allDeps['react-icons'])
|
|
97
|
+
iconLibrary = 'React Icons';
|
|
98
|
+
else if (allDeps['@phosphor-icons/react'])
|
|
99
|
+
iconLibrary = 'Phosphor';
|
|
100
|
+
else if (allDeps['@tabler/icons-react'])
|
|
101
|
+
iconLibrary = 'Tabler Icons';
|
|
102
|
+
else if (allDeps['@radix-ui/react-icons'])
|
|
103
|
+
iconLibrary = 'Radix Icons';
|
|
104
|
+
// State management
|
|
105
|
+
if (allDeps['zustand'])
|
|
106
|
+
stateLibrary = 'Zustand';
|
|
107
|
+
else if (allDeps['@reduxjs/toolkit'] || allDeps['redux'])
|
|
108
|
+
stateLibrary = 'Redux';
|
|
109
|
+
else if (allDeps['jotai'])
|
|
110
|
+
stateLibrary = 'Jotai';
|
|
111
|
+
else if (allDeps['recoil'])
|
|
112
|
+
stateLibrary = 'Recoil';
|
|
113
|
+
else if (allDeps['valtio'])
|
|
114
|
+
stateLibrary = 'Valtio';
|
|
115
|
+
else if (allDeps['pinia'])
|
|
116
|
+
stateLibrary = 'Pinia';
|
|
117
|
+
else if (allDeps['mobx'])
|
|
118
|
+
stateLibrary = 'MobX';
|
|
119
|
+
// Animation
|
|
120
|
+
if (allDeps['framer-motion'] || allDeps['motion'])
|
|
121
|
+
animationLibrary = 'Framer Motion';
|
|
122
|
+
else if (allDeps['@react-spring/web'] || allDeps['react-spring'])
|
|
123
|
+
animationLibrary = 'React Spring';
|
|
124
|
+
else if (allDeps['gsap'])
|
|
125
|
+
animationLibrary = 'GSAP';
|
|
126
|
+
else if (allDeps['animejs'] || allDeps['anime.js'])
|
|
127
|
+
animationLibrary = 'Anime.js';
|
|
128
|
+
else if (allDeps['@formkit/auto-animate'])
|
|
129
|
+
animationLibrary = 'AutoAnimate';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// ignore
|
|
134
|
+
}
|
|
135
|
+
return { iconLibrary, stateLibrary, animationLibrary };
|
|
136
|
+
}
|
|
137
|
+
function scanDirectory(dir, components, rootDir) {
|
|
138
|
+
try {
|
|
139
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (IGNORE_DIRS.includes(entry.name))
|
|
142
|
+
continue;
|
|
143
|
+
const fullPath = path.join(dir, entry.name);
|
|
144
|
+
if (entry.isFile() && COMPONENT_EXTENSIONS.some(ext => entry.name.endsWith(ext))) {
|
|
145
|
+
const component = parseComponent(fullPath, rootDir);
|
|
146
|
+
if (component)
|
|
147
|
+
components.push(component);
|
|
148
|
+
}
|
|
149
|
+
else if (entry.isDirectory()) {
|
|
150
|
+
scanDirectory(fullPath, components, rootDir);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Permission errors
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function parseComponent(filePath, rootDir) {
|
|
159
|
+
let content;
|
|
160
|
+
try {
|
|
161
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
167
|
+
if (['index', 'types', 'utils', 'helpers', 'constants', 'styles', 'hooks', 'context', 'store', 'provider'].includes(fileName.toLowerCase())) {
|
|
168
|
+
if (fileName.toLowerCase() === 'index') {
|
|
169
|
+
const dirName = path.basename(path.dirname(filePath));
|
|
170
|
+
if (dirName === 'components' || dirName === 'ui' || dirName === 'lib')
|
|
171
|
+
return null;
|
|
172
|
+
return parseComponentContent(dirName, content, filePath, rootDir);
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return parseComponentContent(fileName, content, filePath, rootDir);
|
|
177
|
+
}
|
|
178
|
+
function parseComponentContent(name, content, filePath, rootDir) {
|
|
179
|
+
const hasJSX = /<[A-Z][a-zA-Z]*[\s/>]/.test(content) || /return\s*\(?\s*</.test(content);
|
|
180
|
+
const hasExport = /export\s+(default\s+)?/.test(content);
|
|
181
|
+
if (!hasJSX && !hasExport)
|
|
182
|
+
return null;
|
|
183
|
+
const componentName = toPascalCase(name);
|
|
184
|
+
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, '/');
|
|
185
|
+
const variants = extractVariants(content);
|
|
186
|
+
const cssClasses = extractCSSClasses(content);
|
|
187
|
+
const jsxSnippet = extractJSXSnippet(content);
|
|
188
|
+
const props = extractProps(content);
|
|
189
|
+
const category = categorizeComponent(componentName, relativePath, content, cssClasses);
|
|
190
|
+
const { hasAnimation, animationDetails } = detectAnimations(content);
|
|
191
|
+
const statePatterns = detectStatePatterns(content);
|
|
192
|
+
const tailwindPatterns = extractTailwindPatterns(cssClasses);
|
|
193
|
+
return {
|
|
194
|
+
name: componentName,
|
|
195
|
+
filePath: relativePath,
|
|
196
|
+
variants,
|
|
197
|
+
cssClasses,
|
|
198
|
+
jsxSnippet,
|
|
199
|
+
props,
|
|
200
|
+
category,
|
|
201
|
+
hasAnimation,
|
|
202
|
+
animationDetails,
|
|
203
|
+
statePatterns,
|
|
204
|
+
tailwindPatterns,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function categorizeComponent(name, filePath, content, classes) {
|
|
208
|
+
// Check name first
|
|
209
|
+
for (const [cat, pattern] of Object.entries(CATEGORY_PATTERNS)) {
|
|
210
|
+
if (cat === 'other')
|
|
211
|
+
continue;
|
|
212
|
+
if (pattern.test(name))
|
|
213
|
+
return cat;
|
|
214
|
+
}
|
|
215
|
+
// Check file path for hints
|
|
216
|
+
const pathLower = filePath.toLowerCase();
|
|
217
|
+
if (/\/layout[s]?\//.test(pathLower))
|
|
218
|
+
return 'layout';
|
|
219
|
+
if (/\/nav|\/menu|\/header|\/footer/.test(pathLower))
|
|
220
|
+
return 'navigation';
|
|
221
|
+
if (/\/form[s]?\/|\/input[s]?\//.test(pathLower))
|
|
222
|
+
return 'data-input';
|
|
223
|
+
if (/\/modal[s]?\/|\/dialog[s]?\/|\/overlay[s]?\//.test(pathLower))
|
|
224
|
+
return 'overlay';
|
|
225
|
+
if (/\/feedback\/|\/toast|\/alert/.test(pathLower))
|
|
226
|
+
return 'feedback';
|
|
227
|
+
// Check content for category hints
|
|
228
|
+
if (/\<form[\s>]|onSubmit|handleSubmit/i.test(content))
|
|
229
|
+
return 'data-input';
|
|
230
|
+
if (/\<table[\s>]|<thead|<tbody/i.test(content))
|
|
231
|
+
return 'data-display';
|
|
232
|
+
if (/\<nav[\s>]|useRouter|useNavigate|Link\s/i.test(content))
|
|
233
|
+
return 'navigation';
|
|
234
|
+
if (/AnimatePresence|createPortal|usePortal/i.test(content))
|
|
235
|
+
return 'overlay';
|
|
236
|
+
// Check Tailwind classes
|
|
237
|
+
const classStr = classes.join(' ');
|
|
238
|
+
if (/grid-cols|flex.*gap|justify-between|items-center/.test(classStr))
|
|
239
|
+
return 'layout';
|
|
240
|
+
return 'other';
|
|
241
|
+
}
|
|
242
|
+
function detectAnimations(content) {
|
|
243
|
+
const details = [];
|
|
244
|
+
// Framer Motion
|
|
245
|
+
const motionVariants = content.match(/variants\s*=\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g);
|
|
246
|
+
if (motionVariants) {
|
|
247
|
+
for (const v of motionVariants) {
|
|
248
|
+
const trimmed = v.slice(0, 120);
|
|
249
|
+
details.push(`motion-variant: ${trimmed}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (/motion\.|<motion\./.test(content)) {
|
|
253
|
+
details.push('framer-motion');
|
|
254
|
+
// Extract spring configs
|
|
255
|
+
const springMatch = content.match(/spring\s*:\s*\{([^}]+)\}/);
|
|
256
|
+
if (springMatch)
|
|
257
|
+
details.push(`spring: {${springMatch[1].trim()}}`);
|
|
258
|
+
// Extract transition props
|
|
259
|
+
const transitionMatch = content.match(/transition\s*=\s*\{?\{([^}]+)\}/);
|
|
260
|
+
if (transitionMatch)
|
|
261
|
+
details.push(`transition: {${transitionMatch[1].trim()}}`);
|
|
262
|
+
// Detect AnimatePresence
|
|
263
|
+
if (/AnimatePresence/.test(content))
|
|
264
|
+
details.push('animate-presence');
|
|
265
|
+
// Detect layout animations
|
|
266
|
+
if (/layoutId|layout=/.test(content))
|
|
267
|
+
details.push('layout-animation');
|
|
268
|
+
// Extract initial/animate/exit
|
|
269
|
+
const animateMatch = content.match(/animate\s*=\s*\{?\{([^}]+)\}/);
|
|
270
|
+
if (animateMatch)
|
|
271
|
+
details.push(`animate: {${animateMatch[1].trim()}}`);
|
|
272
|
+
}
|
|
273
|
+
// CSS animations
|
|
274
|
+
const animateClasses = content.match(/animate-[\w-]+/g);
|
|
275
|
+
if (animateClasses) {
|
|
276
|
+
details.push(...[...new Set(animateClasses)].map(c => `tw-${c}`));
|
|
277
|
+
}
|
|
278
|
+
// CSS transitions in Tailwind
|
|
279
|
+
const transitionClasses = content.match(/transition-[\w-]+|duration-[\w-]+|ease-[\w-]+/g);
|
|
280
|
+
if (transitionClasses) {
|
|
281
|
+
const unique = [...new Set(transitionClasses)];
|
|
282
|
+
if (unique.length > 0)
|
|
283
|
+
details.push(`tw-transitions: ${unique.join(', ')}`);
|
|
284
|
+
}
|
|
285
|
+
// Hover/focus transforms
|
|
286
|
+
if (/hover:scale|hover:translate|hover:-translate|group-hover:/.test(content)) {
|
|
287
|
+
details.push('hover-transforms');
|
|
288
|
+
}
|
|
289
|
+
return { hasAnimation: details.length > 0, animationDetails: details };
|
|
290
|
+
}
|
|
291
|
+
function detectStatePatterns(content) {
|
|
292
|
+
const patterns = [];
|
|
293
|
+
if (/useState/.test(content))
|
|
294
|
+
patterns.push('useState');
|
|
295
|
+
if (/useReducer/.test(content))
|
|
296
|
+
patterns.push('useReducer');
|
|
297
|
+
if (/useContext/.test(content))
|
|
298
|
+
patterns.push('useContext');
|
|
299
|
+
if (/useStore|create\(/.test(content) && /zustand/i.test(content))
|
|
300
|
+
patterns.push('zustand');
|
|
301
|
+
if (/useQuery|useMutation/.test(content))
|
|
302
|
+
patterns.push('react-query');
|
|
303
|
+
if (/useSWR/.test(content))
|
|
304
|
+
patterns.push('swr');
|
|
305
|
+
if (/useForm/.test(content))
|
|
306
|
+
patterns.push('react-hook-form');
|
|
307
|
+
if (/useRef/.test(content))
|
|
308
|
+
patterns.push('useRef');
|
|
309
|
+
if (/forwardRef/.test(content))
|
|
310
|
+
patterns.push('forwardRef');
|
|
311
|
+
if (/React\.memo|memo\(/.test(content))
|
|
312
|
+
patterns.push('memo');
|
|
313
|
+
return patterns;
|
|
314
|
+
}
|
|
315
|
+
function extractTailwindPatterns(classes) {
|
|
316
|
+
const pattern = {
|
|
317
|
+
backgrounds: [],
|
|
318
|
+
borders: [],
|
|
319
|
+
spacing: [],
|
|
320
|
+
typography: [],
|
|
321
|
+
effects: [],
|
|
322
|
+
layout: [],
|
|
323
|
+
interactive: [],
|
|
324
|
+
};
|
|
325
|
+
for (const cls of classes) {
|
|
326
|
+
if (/^bg-/.test(cls))
|
|
327
|
+
pattern.backgrounds.push(cls);
|
|
328
|
+
else if (/^(border|rounded|ring|outline)/.test(cls))
|
|
329
|
+
pattern.borders.push(cls);
|
|
330
|
+
else if (/^(p-|px-|py-|pt-|pb-|pl-|pr-|m-|mx-|my-|mt-|mb-|ml-|mr-|gap-|space-)/.test(cls))
|
|
331
|
+
pattern.spacing.push(cls);
|
|
332
|
+
else if (/^(text-|font-|tracking-|leading-|truncate|line-clamp)/.test(cls))
|
|
333
|
+
pattern.typography.push(cls);
|
|
334
|
+
else if (/^(shadow|opacity|blur|backdrop|ring|drop-shadow|filter)/.test(cls))
|
|
335
|
+
pattern.effects.push(cls);
|
|
336
|
+
else if (/^(flex|grid|col-|row-|justify|items-|self-|w-|h-|min-|max-|overflow|relative|absolute|fixed|sticky|z-)/.test(cls))
|
|
337
|
+
pattern.layout.push(cls);
|
|
338
|
+
else if (/^(hover:|focus:|active:|disabled:|group-|peer-|cursor-|pointer-events|select-)/.test(cls))
|
|
339
|
+
pattern.interactive.push(cls);
|
|
340
|
+
}
|
|
341
|
+
return pattern;
|
|
342
|
+
}
|
|
343
|
+
function extractVariants(content) {
|
|
344
|
+
const variants = [];
|
|
345
|
+
const unionMatches = content.matchAll(/['"](\w+)['"]\s*\|/g);
|
|
346
|
+
for (const m of unionMatches) {
|
|
347
|
+
if (!variants.includes(m[1]) && isLikelyVariant(m[1]))
|
|
348
|
+
variants.push(m[1]);
|
|
349
|
+
}
|
|
350
|
+
const lastUnionMatches = content.matchAll(/\|\s*['"](\w+)['"]/g);
|
|
351
|
+
for (const m of lastUnionMatches) {
|
|
352
|
+
if (!variants.includes(m[1]) && isLikelyVariant(m[1]))
|
|
353
|
+
variants.push(m[1]);
|
|
354
|
+
}
|
|
355
|
+
// CVA / class-variance-authority
|
|
356
|
+
const cvaMatches = content.matchAll(/variants?\s*:\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/gi);
|
|
357
|
+
for (const m of cvaMatches) {
|
|
358
|
+
const keys = m[1].matchAll(/(\w+)\s*:/g);
|
|
359
|
+
for (const k of keys) {
|
|
360
|
+
if (!variants.includes(k[1]) && isLikelyVariant(k[1]))
|
|
361
|
+
variants.push(k[1]);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return variants;
|
|
365
|
+
}
|
|
366
|
+
function isLikelyVariant(str) {
|
|
367
|
+
const variantKeywords = [
|
|
368
|
+
'primary', 'secondary', 'tertiary', 'ghost', 'outline', 'link',
|
|
369
|
+
'danger', 'warning', 'success', 'info', 'error',
|
|
370
|
+
'sm', 'md', 'lg', 'xl', '2xl',
|
|
371
|
+
'small', 'medium', 'large',
|
|
372
|
+
'default', 'destructive', 'subtle',
|
|
373
|
+
'solid', 'soft', 'plain', 'minimal', 'filled',
|
|
374
|
+
];
|
|
375
|
+
return variantKeywords.includes(str.toLowerCase()) || str.length <= 15;
|
|
376
|
+
}
|
|
377
|
+
function extractCSSClasses(content) {
|
|
378
|
+
const classes = new Set();
|
|
379
|
+
// className="..." or class="..."
|
|
380
|
+
const classMatches = content.matchAll(/class(?:Name)?\s*=\s*["']([^"']+)["']/g);
|
|
381
|
+
for (const m of classMatches) {
|
|
382
|
+
for (const cls of m[1].split(/\s+/)) {
|
|
383
|
+
if (cls.trim())
|
|
384
|
+
classes.add(cls.trim());
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Template literals: className={`...`}
|
|
388
|
+
const templateMatches = content.matchAll(/class(?:Name)?\s*=\s*\{`([^`]+)`\}/g);
|
|
389
|
+
for (const m of templateMatches) {
|
|
390
|
+
const staticParts = m[1].replace(/\$\{[^}]+\}/g, ' ').split(/\s+/);
|
|
391
|
+
for (const cls of staticParts) {
|
|
392
|
+
if (cls.trim())
|
|
393
|
+
classes.add(cls.trim());
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// cn() / clsx() / classnames() / cva()
|
|
397
|
+
const cnMatches = content.matchAll(/(?:cn|clsx|classnames|cva)\s*\(\s*["']([^"']+)["']/g);
|
|
398
|
+
for (const m of cnMatches) {
|
|
399
|
+
for (const cls of m[1].split(/\s+/)) {
|
|
400
|
+
if (cls.trim())
|
|
401
|
+
classes.add(cls.trim());
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// Multi-line cn() calls — capture strings inside
|
|
405
|
+
const cnBlockMatches = content.matchAll(/(?:cn|clsx|classnames)\s*\(([^)]+)\)/gs);
|
|
406
|
+
for (const m of cnBlockMatches) {
|
|
407
|
+
const innerStrings = m[1].matchAll(/["']([^"']+)["']/g);
|
|
408
|
+
for (const s of innerStrings) {
|
|
409
|
+
for (const cls of s[1].split(/\s+/)) {
|
|
410
|
+
if (cls.trim() && !cls.includes('${'))
|
|
411
|
+
classes.add(cls.trim());
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return Array.from(classes).slice(0, 80);
|
|
416
|
+
}
|
|
417
|
+
function extractJSXSnippet(content) {
|
|
418
|
+
const returnMatch = content.match(/return\s*\(\s*\n?([\s\S]*?)\n?\s*\);?/);
|
|
419
|
+
if (returnMatch) {
|
|
420
|
+
const lines = returnMatch[1].split('\n').slice(0, 40);
|
|
421
|
+
return lines.join('\n').trim();
|
|
422
|
+
}
|
|
423
|
+
const arrowMatch = content.match(/=>\s*\(\s*\n?([\s\S]*?)\n?\s*\);?/);
|
|
424
|
+
if (arrowMatch) {
|
|
425
|
+
const lines = arrowMatch[1].split('\n').slice(0, 40);
|
|
426
|
+
return lines.join('\n').trim();
|
|
427
|
+
}
|
|
428
|
+
return '';
|
|
429
|
+
}
|
|
430
|
+
function extractProps(content) {
|
|
431
|
+
const props = [];
|
|
432
|
+
const propsMatch = content.match(/(?:interface|type)\s+\w*Props\w*\s*(?:=\s*)?\{([^}]+)\}/);
|
|
433
|
+
if (propsMatch) {
|
|
434
|
+
const propLines = propsMatch[1].matchAll(/(\w+)\s*[?:]?\s*:/g);
|
|
435
|
+
for (const m of propLines) {
|
|
436
|
+
if (!props.includes(m[1]))
|
|
437
|
+
props.push(m[1]);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const destructMatch = content.match(/\(\s*\{\s*([^}]+)\s*\}\s*(?::\s*\w+)?\s*\)/);
|
|
441
|
+
if (destructMatch) {
|
|
442
|
+
const parts = destructMatch[1].split(',').map(s => s.trim().split(/[\s=:]/)[0].trim());
|
|
443
|
+
for (const p of parts) {
|
|
444
|
+
if (p && !props.includes(p) && /^[a-zA-Z]/.test(p))
|
|
445
|
+
props.push(p);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return props;
|
|
449
|
+
}
|
|
450
|
+
function toPascalCase(str) {
|
|
451
|
+
return str
|
|
452
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
453
|
+
.replace(/^./, s => s.toUpperCase());
|
|
454
|
+
}
|
|
455
|
+
//# sourceMappingURL=components.js.map
|