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.
Files changed (59) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.js +202 -0
  3. package/dist/extractors/components.d.ts +11 -0
  4. package/dist/extractors/components.js +455 -0
  5. package/dist/extractors/framework.d.ts +4 -0
  6. package/dist/extractors/framework.js +126 -0
  7. package/dist/extractors/tokens/computed.d.ts +7 -0
  8. package/dist/extractors/tokens/computed.js +249 -0
  9. package/dist/extractors/tokens/css.d.ts +3 -0
  10. package/dist/extractors/tokens/css.js +510 -0
  11. package/dist/extractors/tokens/http-css.d.ts +14 -0
  12. package/dist/extractors/tokens/http-css.js +1689 -0
  13. package/dist/extractors/tokens/tailwind.d.ts +3 -0
  14. package/dist/extractors/tokens/tailwind.js +353 -0
  15. package/dist/extractors/tokens/tokens-file.d.ts +3 -0
  16. package/dist/extractors/tokens/tokens-file.js +229 -0
  17. package/dist/extractors/ultra/animations.d.ts +21 -0
  18. package/dist/extractors/ultra/animations.js +530 -0
  19. package/dist/extractors/ultra/components-dom.d.ts +13 -0
  20. package/dist/extractors/ultra/components-dom.js +152 -0
  21. package/dist/extractors/ultra/interactions.d.ts +14 -0
  22. package/dist/extractors/ultra/interactions.js +225 -0
  23. package/dist/extractors/ultra/layout.d.ts +14 -0
  24. package/dist/extractors/ultra/layout.js +126 -0
  25. package/dist/extractors/ultra/pages.d.ts +16 -0
  26. package/dist/extractors/ultra/pages.js +231 -0
  27. package/dist/font-resolver.d.ts +10 -0
  28. package/dist/font-resolver.js +280 -0
  29. package/dist/modes/dir.d.ts +6 -0
  30. package/dist/modes/dir.js +213 -0
  31. package/dist/modes/repo.d.ts +6 -0
  32. package/dist/modes/repo.js +76 -0
  33. package/dist/modes/ultra.d.ts +22 -0
  34. package/dist/modes/ultra.js +285 -0
  35. package/dist/modes/url.d.ts +14 -0
  36. package/dist/modes/url.js +161 -0
  37. package/dist/normalizer.d.ts +11 -0
  38. package/dist/normalizer.js +867 -0
  39. package/dist/screenshot.d.ts +9 -0
  40. package/dist/screenshot.js +94 -0
  41. package/dist/types-ultra.d.ts +157 -0
  42. package/dist/types-ultra.js +4 -0
  43. package/dist/types.d.ts +182 -0
  44. package/dist/types.js +4 -0
  45. package/dist/writers/animations-md.d.ts +17 -0
  46. package/dist/writers/animations-md.js +313 -0
  47. package/dist/writers/components-md.d.ts +8 -0
  48. package/dist/writers/components-md.js +151 -0
  49. package/dist/writers/design-md.d.ts +7 -0
  50. package/dist/writers/design-md.js +704 -0
  51. package/dist/writers/interactions-md.d.ts +8 -0
  52. package/dist/writers/interactions-md.js +146 -0
  53. package/dist/writers/layout-md.d.ts +8 -0
  54. package/dist/writers/layout-md.js +120 -0
  55. package/dist/writers/skill.d.ts +12 -0
  56. package/dist/writers/skill.js +1006 -0
  57. package/dist/writers/tokens-json.d.ts +11 -0
  58. package/dist/writers/tokens-json.js +164 -0
  59. package/package.json +78 -0
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
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
@@ -0,0 +1,4 @@
1
+ import { Framework } from '../types';
2
+ export declare function detectFrameworks(projectDir: string): Framework[];
3
+ export declare function getProjectName(projectDir: string, overrideName?: string): string;
4
+ //# sourceMappingURL=framework.d.ts.map