designlang 4.0.0 → 5.0.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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <h1 align="center">designlang</h1>
2
+ <h1 align="center">DESIGNLANG</h1>
3
3
  <p align="center">Reverse-engineer any website's complete design system in one command.</p>
4
4
  </p>
5
5
 
@@ -7,6 +7,7 @@
7
7
  <a href="https://www.npmjs.com/package/designlang"><img src="https://img.shields.io/npm/v/designlang?color=blue&label=npm" alt="npm version"></a>
8
8
  <a href="https://github.com/Manavarya09/design-extract/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Manavarya09/design-extract" alt="license"></a>
9
9
  <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/designlang" alt="node version"></a>
10
+ <a href="https://website-five-lime-65.vercel.app"><img src="https://img.shields.io/badge/website-live-red" alt="website"></a>
10
11
  </p>
11
12
 
12
13
  ---
@@ -127,6 +128,47 @@ designlang brands stripe.com vercel.com github.com linear.app
127
128
 
128
129
  Generates a matrix with color overlap analysis, typography comparison, spacing systems, and accessibility scores. Outputs both `brands.md` and `brands.html`.
129
130
 
131
+ ### 6. Clone Command
132
+
133
+ Generate a working Next.js app with the extracted design applied:
134
+
135
+ ```bash
136
+ designlang clone https://stripe.com
137
+ cd cloned-design && npm install && npm run dev
138
+ ```
139
+
140
+ One command → a running app with the site's colors, fonts, spacing, and component patterns.
141
+
142
+ ### 7. Design System Scoring
143
+
144
+ Rate any site's design quality across 7 categories:
145
+
146
+ ```bash
147
+ designlang score https://vercel.com
148
+ ```
149
+
150
+ ```
151
+ 68/100 Grade: D
152
+
153
+ Color Discipline ██████████░░░░░░░░░░ 50
154
+ Typography ██████████████░░░░░░ 70
155
+ Spacing System ████████████████░░░░ 80
156
+ Shadows ██████████░░░░░░░░░░ 50
157
+ Border Radii ████████░░░░░░░░░░░░ 40
158
+ Accessibility ███████████████████░ 94
159
+ Tokenization ████████████████████ 100
160
+ ```
161
+
162
+ ### 8. Watch Mode
163
+
164
+ Monitor a site for design changes:
165
+
166
+ ```bash
167
+ designlang watch https://stripe.com --interval 60
168
+ ```
169
+
170
+ Checks hourly and alerts when colors, fonts, or accessibility scores change.
171
+
130
172
  ## All Features
131
173
 
132
174
  | Feature | Flag / Command | Description |
@@ -134,12 +176,16 @@ Generates a matrix with color overlap analysis, typography comparison, spacing s
134
176
  | Base extraction | `designlang <url>` | Colors, typography, spacing, shadows, radii, CSS vars, breakpoints, animations, components |
135
177
  | Layout system | automatic | Grid patterns, flex usage, container widths, gap values |
136
178
  | Accessibility | automatic | WCAG 2.1 contrast ratios for all fg/bg pairs |
179
+ | Design scoring | automatic | 7-category quality rating (A-F) with actionable issues |
137
180
  | Dark mode | `--dark` | Extracts dark color scheme |
138
181
  | Multi-page | `--depth <n>` | Crawl N internal pages for site-wide tokens |
139
182
  | Screenshots | `--screenshots` | Capture buttons, cards, inputs, nav, hero, full page |
140
183
  | Responsive | `--responsive` | Crawl at 4 viewports, map breakpoint changes |
141
184
  | Interactions | `--interactions` | Capture hover/focus/active state transitions |
142
185
  | Everything | `--full` | Enable screenshots + responsive + interactions |
186
+ | Clone | `designlang clone <url>` | Generate a working Next.js starter with extracted design |
187
+ | Score | `designlang score <url>` | Rate design quality with visual bar chart breakdown |
188
+ | Watch | `designlang watch <url>` | Monitor for design changes on interval |
143
189
  | Diff | `designlang diff <A> <B>` | Compare two sites (MD + HTML) |
144
190
  | Multi-brand | `designlang brands <urls...>` | N-site comparison matrix |
145
191
  | Sync | `designlang sync <url>` | Update local tokens from live site |
@@ -167,6 +213,9 @@ Options:
167
213
  --verbose Detailed progress output
168
214
 
169
215
  Commands:
216
+ clone <url> Generate a working Next.js starter from extracted design
217
+ score <url> Rate design quality (7 categories, A-F, bar chart)
218
+ watch <url> Monitor for design changes on interval
170
219
  diff <urlA> <urlB> Compare two sites' design languages
171
220
  brands <urls...> Multi-brand comparison matrix
172
221
  sync <url> Sync local tokens with live site
@@ -203,12 +252,13 @@ Running `designlang https://vercel.com --full`:
203
252
  Shadows: 11 unique shadows
204
253
  Radii: 10 unique values
205
254
  Breakpoints: 45 breakpoints
206
- Components: 4 patterns detected
255
+ Components: 11 types detected
207
256
  CSS Vars: 407 custom properties
208
257
  Layout: 55 grids, 492 flex containers
209
258
  Responsive: 4 viewports, 3 breakpoint changes
210
259
  Interactions: 8 state changes captured
211
260
  A11y: 94% WCAG score (7 failing pairs)
261
+ Design Score: 68/100 (D) — 4 issues
212
262
  ```
213
263
 
214
264
  ## How It Works
@@ -230,6 +280,10 @@ npx skills add Manavarya09/design-extract
230
280
 
231
281
  In Claude Code, use `/extract-design <url>`.
232
282
 
283
+ ## Website
284
+
285
+ **[website-five-lime-65.vercel.app](https://website-five-lime-65.vercel.app)** — the brutalist product page.
286
+
233
287
  ## Contributing
234
288
 
235
289
  See [CONTRIBUTING.md](CONTRIBUTING.md). PRs welcome!
@@ -21,6 +21,8 @@ import { syncDesign } from '../src/sync.js';
21
21
  import { compareBrands, formatBrandMatrix, formatBrandMatrixHtml } from '../src/multibrand.js';
22
22
  import { generateClone } from '../src/clone.js';
23
23
  import { watchSite } from '../src/watch.js';
24
+ import { diffDarkMode } from '../src/darkdiff.js';
25
+ import { applyDesign } from '../src/apply.js';
24
26
  import { nameFromUrl } from '../src/utils.js';
25
27
 
26
28
  const program = new Command();
@@ -28,7 +30,7 @@ const program = new Command();
28
30
  program
29
31
  .name('designlang')
30
32
  .description('Extract the complete design language from any website')
31
- .version('4.0.0');
33
+ .version('5.0.0');
32
34
 
33
35
  // ── Main command: extract ──────────────────────────────────────
34
36
  program
@@ -45,6 +47,8 @@ program
45
47
  .option('--responsive', 'capture design at multiple breakpoints')
46
48
  .option('--interactions', 'capture hover/focus/active states')
47
49
  .option('--full', 'enable all extra captures (screenshots, responsive, interactions)')
50
+ .option('--cookie <cookies...>', 'cookies for authenticated pages (name=value)')
51
+ .option('--header <headers...>', 'custom headers (name:value)')
48
52
  .option('--no-history', 'skip saving to history')
49
53
  .option('--verbose', 'show detailed progress')
50
54
  .action(async (url, opts) => {
@@ -61,6 +65,16 @@ program
61
65
 
62
66
  try {
63
67
  spinner.text = `Crawling${opts.depth > 0 ? ` (depth: ${opts.depth})` : ''}...`;
68
+ // Parse auth options
69
+ const cookies = opts.cookie || [];
70
+ const headers = {};
71
+ if (opts.header) {
72
+ for (const h of opts.header) {
73
+ const [name, ...rest] = h.split(':');
74
+ if (name && rest.length) headers[name.trim()] = rest.join(':').trim();
75
+ }
76
+ }
77
+
64
78
  const design = await extractDesignLanguage(url, {
65
79
  width: opts.width,
66
80
  height: parseInt(opts.height) || 800,
@@ -69,6 +83,8 @@ program
69
83
  depth: opts.depth,
70
84
  screenshots: opts.screenshots || opts.full,
71
85
  outDir,
86
+ cookies: cookies.length > 0 ? cookies : undefined,
87
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
72
88
  });
73
89
 
74
90
  // Responsive capture
@@ -163,6 +179,25 @@ program
163
179
  console.log(` ${chalk.gray('Design Score:')} ${gradeColor(`${s.overall}/100 (${s.grade})`)}${s.issues.length > 0 ? ` — ${s.issues.length} issues` : ''}`);
164
180
  }
165
181
 
182
+ // New v5 extractors
183
+ if (design.gradients && design.gradients.count > 0) {
184
+ console.log(` ${chalk.gray('Gradients:')} ${design.gradients.count} unique gradients`);
185
+ }
186
+ if (design.zIndex && design.zIndex.allValues.length > 0) {
187
+ console.log(` ${chalk.gray('Z-Index:')} ${design.zIndex.allValues.length} layers${design.zIndex.issues.length > 0 ? ` (${design.zIndex.issues.length} issues)` : ''}`);
188
+ }
189
+ if (design.icons && design.icons.count > 0) {
190
+ console.log(` ${chalk.gray('Icons:')} ${design.icons.count} SVG icons (${design.icons.dominantStyle || 'mixed'})`);
191
+ }
192
+ if (design.fonts && design.fonts.fonts.length > 0) {
193
+ const sources = design.fonts.fonts.map(f => f.source).filter((v, i, a) => a.indexOf(v) === i);
194
+ console.log(` ${chalk.gray('Font Files:')} ${design.fonts.fonts.length} fonts (${sources.join(', ')})`);
195
+ }
196
+ if (design.images && design.images.patterns.length > 0) {
197
+ const total = design.images.patterns.reduce((s, p) => s + p.count, 0);
198
+ console.log(` ${chalk.gray('Images:')} ${total} images, ${design.images.patterns.length} style patterns`);
199
+ }
200
+
166
201
  // Accessibility summary
167
202
  if (design.accessibility) {
168
203
  const a = design.accessibility;
@@ -486,4 +521,44 @@ program
486
521
  }
487
522
  });
488
523
 
524
+ // ── Apply command ──────────────────────────────────────────
525
+ program
526
+ .command('apply <url>')
527
+ .description('Extract and apply design directly to your project')
528
+ .option('-d, --dir <path>', 'project directory', '.')
529
+ .option('--framework <type>', 'force framework (tailwind, shadcn, css)')
530
+ .option('--cookie <cookies...>', 'cookies for authenticated pages')
531
+ .option('--header <headers...>', 'custom headers')
532
+ .action(async (url, opts) => {
533
+ if (!url.startsWith('http')) url = `https://${url}`;
534
+
535
+ console.log('');
536
+ console.log(chalk.bold(' designlang apply'));
537
+ console.log(chalk.gray(` ${url} → ${resolve(opts.dir)}`));
538
+ console.log('');
539
+
540
+ const spinner = ora('Extracting design...').start();
541
+
542
+ try {
543
+ const result = await applyDesign(url, {
544
+ dir: resolve(opts.dir),
545
+ framework: opts.framework,
546
+ cookies: opts.cookie,
547
+ headers: opts.header ? Object.fromEntries(opts.header.map(h => { const [k, ...v] = h.split(':'); return [k.trim(), v.join(':').trim()]; })) : undefined,
548
+ });
549
+
550
+ spinner.succeed(`Applied ${result.framework} design!`);
551
+ console.log('');
552
+ for (const f of result.applied) {
553
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(f.file)} — ${f.type}`);
554
+ }
555
+ console.log('');
556
+
557
+ } catch (err) {
558
+ spinner.fail('Apply failed');
559
+ console.error(chalk.red(`\n ${err.message}\n`));
560
+ process.exit(1);
561
+ }
562
+ });
563
+
489
564
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "designlang",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Extract the complete design language from any website — colors, typography, spacing, shadows, and more. Outputs AI-optimized markdown, W3C design tokens, Tailwind config, and CSS variables.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/apply.js ADDED
@@ -0,0 +1,65 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { extractDesignLanguage } from './index.js';
4
+ import { formatTailwind } from './formatters/tailwind.js';
5
+ import { formatCssVars } from './formatters/css-vars.js';
6
+ import { formatShadcnTheme } from './formatters/theme.js';
7
+
8
+ export async function applyDesign(url, options = {}) {
9
+ const { dir = '.', framework } = options;
10
+ const design = await extractDesignLanguage(url, options);
11
+ const detected = framework || detectFramework(dir);
12
+ const applied = [];
13
+
14
+ if (detected === 'tailwind' || detected === 'auto') {
15
+ const twPath = findFile(dir, ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.mjs']);
16
+ if (twPath) {
17
+ writeFileSync(twPath, formatTailwind(design), 'utf-8');
18
+ applied.push({ file: twPath, type: 'tailwind' });
19
+ }
20
+ }
21
+
22
+ if (detected === 'shadcn' || detected === 'auto') {
23
+ const globalsPath = findFile(dir, [
24
+ 'app/globals.css', 'src/app/globals.css', 'styles/globals.css',
25
+ 'src/styles/globals.css', 'src/index.css', 'app/global.css',
26
+ ]);
27
+ if (globalsPath) {
28
+ const existing = readFileSync(globalsPath, 'utf-8');
29
+ const shadcnVars = formatShadcnTheme(design);
30
+ // Replace existing @layer base block or append
31
+ const layerRegex = /@layer\s+base\s*\{[\s\S]*?\n\}/;
32
+ const updated = layerRegex.test(existing)
33
+ ? existing.replace(layerRegex, shadcnVars)
34
+ : existing + '\n\n' + shadcnVars;
35
+ writeFileSync(globalsPath, updated, 'utf-8');
36
+ applied.push({ file: globalsPath, type: 'shadcn' });
37
+ }
38
+ }
39
+
40
+ if (detected === 'css' || detected === 'auto') {
41
+ const cssVarsContent = formatCssVars(design);
42
+ const cssPath = join(dir, 'design-variables.css');
43
+ writeFileSync(cssPath, cssVarsContent, 'utf-8');
44
+ applied.push({ file: cssPath, type: 'css-variables' });
45
+ }
46
+
47
+ return { design, applied, framework: detected };
48
+ }
49
+
50
+ function detectFramework(dir) {
51
+ if (findFile(dir, ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.mjs'])) {
52
+ // Check for shadcn
53
+ if (findFile(dir, ['components.json'])) return 'shadcn';
54
+ return 'tailwind';
55
+ }
56
+ return 'auto';
57
+ }
58
+
59
+ function findFile(dir, candidates) {
60
+ for (const c of candidates) {
61
+ const p = join(dir, c);
62
+ if (existsSync(p)) return p;
63
+ }
64
+ return null;
65
+ }
package/src/crawler.js CHANGED
@@ -5,18 +5,36 @@ import { join } from 'path';
5
5
  const MAX_ELEMENTS = 5000;
6
6
 
7
7
  export async function crawlPage(url, options = {}) {
8
- const { width = 1280, height = 800, wait = 0, dark = false, depth = 0, screenshots = false, outDir = '' } = options;
8
+ const { width = 1280, height = 800, wait = 0, dark = false, depth = 0, screenshots = false, outDir = '', executablePath, browserArgs, cookies, headers } = options;
9
9
 
10
- const browser = await chromium.launch({ headless: true });
10
+ const browser = await chromium.launch({
11
+ headless: true,
12
+ ...(executablePath && { executablePath }),
13
+ ...(browserArgs && { args: browserArgs }),
14
+ });
11
15
  const context = await browser.newContext({
12
16
  viewport: { width, height },
13
17
  colorScheme: 'light',
18
+ ...(headers && { extraHTTPHeaders: headers }),
14
19
  });
20
+
21
+ // Set cookies if provided
22
+ if (cookies && cookies.length > 0) {
23
+ await context.addCookies(cookies.map(c => {
24
+ if (typeof c === 'string') {
25
+ const [name, ...rest] = c.split('=');
26
+ return { name, value: rest.join('='), url };
27
+ }
28
+ return c;
29
+ }));
30
+ }
15
31
  const page = await context.newPage();
16
32
 
17
- await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
33
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
34
+ // Wait for network to settle — but don't hang on sites with persistent connections
35
+ await page.waitForLoadState('networkidle').catch(() => {});
18
36
  if (wait > 0) await page.waitForTimeout(wait);
19
- await page.evaluate(() => document.fonts.ready);
37
+ await page.evaluate(() => document.fonts.ready).catch(() => {});
20
38
 
21
39
  const title = await page.title();
22
40
  const lightData = await extractPageData(page);
@@ -33,8 +51,9 @@ export async function crawlPage(url, options = {}) {
33
51
  const internalLinks = await discoverInternalLinks(page, url, depth);
34
52
  for (const link of internalLinks) {
35
53
  try {
36
- await page.goto(link, { waitUntil: 'networkidle', timeout: 20000 });
37
- await page.evaluate(() => document.fonts.ready);
54
+ await page.goto(link, { waitUntil: 'domcontentloaded', timeout: 20000 });
55
+ await page.waitForLoadState('networkidle').catch(() => {});
56
+ await page.evaluate(() => document.fonts.ready).catch(() => {});
38
57
  const pageData = await extractPageData(page);
39
58
  additionalPages.push({ url: link, data: pageData });
40
59
  } catch { /* skip failed pages */ }
@@ -50,8 +69,9 @@ export async function crawlPage(url, options = {}) {
50
69
  colorScheme: 'dark',
51
70
  });
52
71
  const darkPage = await darkContext.newPage();
53
- await darkPage.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
54
- await darkPage.evaluate(() => document.fonts.ready);
72
+ await darkPage.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
73
+ await darkPage.waitForLoadState('networkidle').catch(() => {});
74
+ await darkPage.evaluate(() => document.fonts.ready).catch(() => {});
55
75
  darkData = await extractPageData(darkPage);
56
76
  await darkContext.close();
57
77
  } else {
@@ -269,6 +289,69 @@ async function extractPageData(page) {
269
289
  }
270
290
  } catch { /* no access */ }
271
291
 
292
+ // SVG icons
293
+ results.icons = [];
294
+ for (const svg of document.querySelectorAll('svg')) {
295
+ const rect = svg.getBoundingClientRect();
296
+ if (rect.width > 4 && rect.width < 200 && rect.height > 4 && rect.height < 200) {
297
+ results.icons.push({
298
+ svg: svg.outerHTML,
299
+ width: rect.width,
300
+ height: rect.height,
301
+ viewBox: svg.getAttribute('viewBox') || '',
302
+ classList: Array.from(svg.classList).join(' '),
303
+ fill: svg.getAttribute('fill') || getComputedStyle(svg).fill || '',
304
+ stroke: svg.getAttribute('stroke') || getComputedStyle(svg).stroke || '',
305
+ });
306
+ }
307
+ }
308
+
309
+ // Font data
310
+ results.fontData = { fontFaces: [], googleFontsLinks: [], documentFonts: [] };
311
+ try {
312
+ for (const sheet of document.styleSheets) {
313
+ try {
314
+ for (const rule of sheet.cssRules) {
315
+ if (rule instanceof CSSFontFaceRule) {
316
+ results.fontData.fontFaces.push({
317
+ family: rule.style.getPropertyValue('font-family').replace(/['"]/g, ''),
318
+ style: rule.style.getPropertyValue('font-style') || 'normal',
319
+ weight: rule.style.getPropertyValue('font-weight') || '400',
320
+ src: rule.style.getPropertyValue('src') || '',
321
+ });
322
+ }
323
+ }
324
+ } catch { /* cross-origin */ }
325
+ }
326
+ } catch {}
327
+ for (const link of document.querySelectorAll('link[href*="fonts.googleapis.com"]')) {
328
+ results.fontData.googleFontsLinks.push(link.href);
329
+ }
330
+ for (const font of document.fonts) {
331
+ results.fontData.documentFonts.push({ family: font.family.replace(/['"]/g, ''), style: font.style, weight: font.weight, status: font.status });
332
+ }
333
+
334
+ // Image data
335
+ results.images = [];
336
+ for (const img of document.querySelectorAll('img, picture img, [role="img"]')) {
337
+ const rect = img.getBoundingClientRect();
338
+ if (rect.width < 5 || rect.height < 5) continue;
339
+ const cs = getComputedStyle(img);
340
+ results.images.push({
341
+ tag: img.tagName.toLowerCase(),
342
+ src: img.src || '',
343
+ width: rect.width,
344
+ height: rect.height,
345
+ objectFit: cs.objectFit,
346
+ objectPosition: cs.objectPosition,
347
+ borderRadius: cs.borderRadius,
348
+ filter: cs.filter,
349
+ opacity: cs.opacity,
350
+ aspectRatio: cs.aspectRatio,
351
+ classList: Array.from(img.classList).join(' '),
352
+ });
353
+ }
354
+
272
355
  return results;
273
356
  }, MAX_ELEMENTS);
274
357
  }
@@ -0,0 +1,65 @@
1
+ export function diffDarkMode(lightDesign, darkDesign) {
2
+ const changes = [];
3
+
4
+ // Color changes
5
+ const lightColors = new Set(lightDesign.colors.all.map(c => c.hex));
6
+ const darkColors = new Set(darkDesign.colors.all.map(c => c.hex));
7
+
8
+ const addedInDark = darkDesign.colors.all.filter(c => !lightColors.has(c.hex));
9
+ const removedInDark = lightDesign.colors.all.filter(c => !darkColors.has(c.hex));
10
+
11
+ if (addedInDark.length > 0 || removedInDark.length > 0) {
12
+ changes.push({
13
+ category: 'colors',
14
+ light: lightDesign.colors.all.length,
15
+ dark: darkDesign.colors.all.length,
16
+ added: addedInDark.map(c => c.hex),
17
+ removed: removedInDark.map(c => c.hex),
18
+ });
19
+ }
20
+
21
+ // CSS variable changes
22
+ const lightVars = flattenVars(lightDesign.variables);
23
+ const darkVars = flattenVars(darkDesign.variables);
24
+ const varChanges = [];
25
+
26
+ for (const [key, lightVal] of Object.entries(lightVars)) {
27
+ const darkVal = darkVars[key];
28
+ if (darkVal && darkVal !== lightVal) {
29
+ varChanges.push({ name: key, light: lightVal, dark: darkVal });
30
+ }
31
+ }
32
+ const newDarkVars = Object.entries(darkVars)
33
+ .filter(([key]) => !lightVars[key])
34
+ .map(([name, dark]) => ({ name, light: null, dark }));
35
+
36
+ if (varChanges.length > 0 || newDarkVars.length > 0) {
37
+ changes.push({
38
+ category: 'cssVariables',
39
+ changed: varChanges,
40
+ newInDark: newDarkVars,
41
+ });
42
+ }
43
+
44
+ return {
45
+ hasChanges: changes.length > 0,
46
+ changes,
47
+ summary: {
48
+ colorsChanged: (addedInDark.length + removedInDark.length) || 0,
49
+ variablesChanged: varChanges.length,
50
+ newDarkVariables: newDarkVars.length,
51
+ },
52
+ };
53
+ }
54
+
55
+ function flattenVars(variables) {
56
+ const flat = {};
57
+ for (const [, group] of Object.entries(variables)) {
58
+ if (typeof group === 'object') {
59
+ for (const [key, val] of Object.entries(group)) {
60
+ flat[key] = val;
61
+ }
62
+ }
63
+ }
64
+ return flat;
65
+ }
@@ -2,6 +2,8 @@ export function extractAnimations(computedStyles, keyframes) {
2
2
  const transitionSet = new Set();
3
3
  const easingSet = new Set();
4
4
  const durationSet = new Set();
5
+ const animationNames = new Set();
6
+ const transitionProperties = {};
5
7
 
6
8
  for (const el of computedStyles) {
7
9
  if (el.transition && el.transition !== 'all 0s ease 0s' && el.transition !== 'none') {
@@ -13,16 +15,50 @@ export function extractAnimations(computedStyles, keyframes) {
13
15
 
14
16
  const eMatch = el.transition.match(/(ease|ease-in|ease-out|ease-in-out|linear|cubic-bezier\([^)]+\))/g);
15
17
  if (eMatch) eMatch.forEach(e => easingSet.add(e));
18
+
19
+ // Extract which properties are animated
20
+ const parts = el.transition.split(',').map(s => s.trim());
21
+ for (const part of parts) {
22
+ const prop = part.split(/\s+/)[0];
23
+ if (prop && prop !== 'all') {
24
+ transitionProperties[prop] = (transitionProperties[prop] || 0) + 1;
25
+ }
26
+ }
27
+ }
28
+
29
+ // Capture animation usage
30
+ if (el.animation && el.animation !== 'none 0s ease 0s 1 normal none running' && el.animation !== 'none') {
31
+ const nameMatch = el.animation.match(/^([\w-]+)/);
32
+ if (nameMatch && nameMatch[1] !== 'none') animationNames.add(nameMatch[1]);
16
33
  }
17
34
  }
18
35
 
36
+ // Enhanced keyframes with timing and properties changed
37
+ const enhancedKeyframes = keyframes.map(kf => {
38
+ const propertiesAnimated = new Set();
39
+ for (const step of kf.steps) {
40
+ const props = step.style.split(';').map(s => s.split(':')[0].trim()).filter(Boolean);
41
+ props.forEach(p => propertiesAnimated.add(p));
42
+ }
43
+ return {
44
+ name: kf.name,
45
+ steps: kf.steps,
46
+ propertiesAnimated: [...propertiesAnimated],
47
+ isUsed: animationNames.has(kf.name),
48
+ };
49
+ });
50
+
51
+ // Sort transition properties by usage
52
+ const sortedProps = Object.entries(transitionProperties)
53
+ .sort((a, b) => b[1] - a[1])
54
+ .map(([prop, count]) => ({ property: prop, count }));
55
+
19
56
  return {
20
57
  transitions: [...transitionSet],
21
58
  easings: [...easingSet],
22
59
  durations: [...durationSet],
23
- keyframes: keyframes.map(kf => ({
24
- name: kf.name,
25
- steps: kf.steps,
26
- })),
60
+ keyframes: enhancedKeyframes,
61
+ transitionProperties: sortedProps,
62
+ animationNames: [...animationNames],
27
63
  };
28
64
  }
@@ -131,9 +131,32 @@ export function extractComponents(computedStyles) {
131
131
  };
132
132
  }
133
133
 
134
+ // Generate CSS snippets for each component
135
+ for (const [type, data] of Object.entries(components)) {
136
+ if (data.baseStyle) {
137
+ const style = type === 'tables' ? { ...data.baseStyle } : data.baseStyle;
138
+ delete style.cellStyle;
139
+ data.css = styleToCss(`.${type.replace(/s$/, '')}`, style);
140
+ }
141
+ }
142
+
134
143
  return components;
135
144
  }
136
145
 
146
+ function styleToCss(selector, style) {
147
+ const propMap = {
148
+ backgroundColor: 'background-color', color: 'color', fontSize: 'font-size',
149
+ fontWeight: 'font-weight', paddingTop: 'padding-top', paddingRight: 'padding-right',
150
+ paddingBottom: 'padding-bottom', paddingLeft: 'padding-left',
151
+ borderRadius: 'border-radius', boxShadow: 'box-shadow', borderColor: 'border-color',
152
+ maxWidth: 'max-width', position: 'position',
153
+ };
154
+ const lines = Object.entries(style)
155
+ .filter(([, v]) => v)
156
+ .map(([k, v]) => ` ${propMap[k] || k}: ${v};`);
157
+ return `${selector} {\n${lines.join('\n')}\n}`;
158
+ }
159
+
137
160
  function mostCommonStyle(elements, properties) {
138
161
  const style = {};
139
162
  for (const prop of properties) {