designlang 1.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/.claude-plugin/marketplace.json +28 -0
- package/.claude-plugin/plugin.json +24 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/design-extract.js +104 -0
- package/package.json +39 -0
- package/skills/extract-design/SKILL.md +59 -0
- package/src/crawler.js +162 -0
- package/src/extractors/animations.js +28 -0
- package/src/extractors/borders.js +31 -0
- package/src/extractors/breakpoints.js +33 -0
- package/src/extractors/colors.js +87 -0
- package/src/extractors/components.js +66 -0
- package/src/extractors/shadows.js +27 -0
- package/src/extractors/spacing.js +37 -0
- package/src/extractors/typography.js +72 -0
- package/src/extractors/variables.js +22 -0
- package/src/formatters/css-vars.js +103 -0
- package/src/formatters/markdown.js +305 -0
- package/src/formatters/tailwind.js +81 -0
- package/src/formatters/tokens.js +61 -0
- package/src/index.js +48 -0
- package/src/utils.js +179 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "design-extract",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Manavarya Singh",
|
|
5
|
+
"url": "https://github.com/Manavarya09"
|
|
6
|
+
},
|
|
7
|
+
"plugins": [
|
|
8
|
+
{
|
|
9
|
+
"name": "design-extract",
|
|
10
|
+
"source": "./",
|
|
11
|
+
"description": "Extract the complete design language from any website — colors, typography, spacing, shadows, components, and more. Outputs AI-optimized markdown, W3C design tokens, Tailwind config, and CSS variables.",
|
|
12
|
+
"version": "1.0.0",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Manavarya Singh"
|
|
15
|
+
},
|
|
16
|
+
"category": "design",
|
|
17
|
+
"tags": [
|
|
18
|
+
"design-system",
|
|
19
|
+
"design-tokens",
|
|
20
|
+
"css",
|
|
21
|
+
"tailwind",
|
|
22
|
+
"typography",
|
|
23
|
+
"colors",
|
|
24
|
+
"web-scraping"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "design-extract",
|
|
3
|
+
"description": "Extract the complete design language from any website. Produces W3C design tokens, AI-optimized markdown, Tailwind config, and CSS custom properties.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Manavarya Singh",
|
|
7
|
+
"url": "https://github.com/Manavarya09"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/Manavarya09/design-extract",
|
|
10
|
+
"repository": "https://github.com/Manavarya09/design-extract",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"design-system",
|
|
14
|
+
"design-tokens",
|
|
15
|
+
"design-language",
|
|
16
|
+
"css",
|
|
17
|
+
"tailwind",
|
|
18
|
+
"playwright",
|
|
19
|
+
"extraction",
|
|
20
|
+
"colors",
|
|
21
|
+
"typography"
|
|
22
|
+
],
|
|
23
|
+
"skills": "./skills/"
|
|
24
|
+
}
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Contributing to design-ex
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing! Here's how to get started.
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/Manavarya09/design-extract.git
|
|
9
|
+
cd design-extract
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This installs dependencies and Playwright's Chromium browser.
|
|
14
|
+
|
|
15
|
+
## Running Locally
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
node bin/design-extract.js https://example.com --out ./test-output
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Project Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
src/
|
|
25
|
+
crawler.js # Playwright page.evaluate extraction
|
|
26
|
+
index.js # Orchestrator
|
|
27
|
+
utils.js # Color parsing, clustering, helpers
|
|
28
|
+
extractors/ # 9 modules that process raw style data
|
|
29
|
+
colors.js # Color palette extraction
|
|
30
|
+
typography.js # Font and type scale extraction
|
|
31
|
+
spacing.js # Spacing scale detection
|
|
32
|
+
shadows.js # Box shadow parsing
|
|
33
|
+
borders.js # Border radius extraction
|
|
34
|
+
variables.js # CSS custom property categorization
|
|
35
|
+
breakpoints.js # Media query extraction
|
|
36
|
+
animations.js # Transition and keyframe extraction
|
|
37
|
+
components.js # UI component pattern detection
|
|
38
|
+
formatters/ # 4 output format generators
|
|
39
|
+
markdown.js # AI-optimized markdown (hero output)
|
|
40
|
+
tokens.js # W3C Design Tokens JSON
|
|
41
|
+
tailwind.js # Tailwind CSS config
|
|
42
|
+
css-vars.js # CSS custom properties file
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Guidelines
|
|
46
|
+
|
|
47
|
+
- Keep dependencies minimal — prefer pure JS over adding a package
|
|
48
|
+
- Test changes against at least 2-3 real websites
|
|
49
|
+
- Follow the existing code style (ES modules, no semicolons in some files)
|
|
50
|
+
- The markdown formatter is the most important output — keep it rich and AI-friendly
|
|
51
|
+
|
|
52
|
+
## Reporting Issues
|
|
53
|
+
|
|
54
|
+
When filing a bug, please include:
|
|
55
|
+
- The URL you tried to extract from
|
|
56
|
+
- The error message or unexpected output
|
|
57
|
+
- Your Node.js version (`node --version`)
|
|
58
|
+
|
|
59
|
+
## Pull Requests
|
|
60
|
+
|
|
61
|
+
- One feature/fix per PR
|
|
62
|
+
- Keep PRs focused and small when possible
|
|
63
|
+
- Add a brief description of what changed and why
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Manavarya Singh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center">designlang</h1>
|
|
3
|
+
<p align="center">Extract the complete design language from any website.</p>
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
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
|
+
<a href="https://github.com/Manavarya09/designlangtract/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Manavarya09/designlangtract" alt="license"></a>
|
|
9
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/node/v/designlang" alt="node version"></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
**designlang** uses Playwright to headlessly crawl any website, extracts computed styles from the live DOM, and generates a full design system you can immediately use in your project. The primary output is an **AI-optimized markdown file** that an LLM can use to faithfully recreate the design.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx designlang https://stripe.com
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That's it. Four files appear in `./designlangtract-output/`:
|
|
23
|
+
|
|
24
|
+
| File | What it is |
|
|
25
|
+
|------|------------|
|
|
26
|
+
| `*-design-language.md` | AI-optimized markdown — the full design system in natural language with code examples |
|
|
27
|
+
| `*-design-tokens.json` | [W3C Design Tokens](https://design-tokens.github.io/community-group/format/) format for tooling |
|
|
28
|
+
| `*-tailwind.config.js` | Drop-in Tailwind CSS theme extension |
|
|
29
|
+
| `*-variables.css` | CSS custom properties ready to import |
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Use directly with npx (no install needed)
|
|
35
|
+
npx designlang https://example.com
|
|
36
|
+
|
|
37
|
+
# Or install globally
|
|
38
|
+
npm install -g designlang
|
|
39
|
+
designlang https://example.com
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> Playwright's Chromium is auto-installed on first run via the `postinstall` script.
|
|
43
|
+
|
|
44
|
+
## What It Extracts
|
|
45
|
+
|
|
46
|
+
| Category | Details |
|
|
47
|
+
|----------|---------|
|
|
48
|
+
| **Colors** | Full palette with primary/secondary/accent/neutral classification, gradients, background & text colors |
|
|
49
|
+
| **Typography** | Font families, type scale (heading/body/caption), weights, line heights, letter spacing |
|
|
50
|
+
| **Spacing** | All unique values with automatic base-unit detection (e.g. 4px grid) |
|
|
51
|
+
| **Border Radii** | Unique values labeled xs through full |
|
|
52
|
+
| **Box Shadows** | Parsed and classified by visual weight (xs/sm/md/lg/xl) |
|
|
53
|
+
| **CSS Variables** | All `:root` custom properties, categorized by type |
|
|
54
|
+
| **Breakpoints** | Media query breakpoints with standard labels (sm/md/lg/xl) |
|
|
55
|
+
| **Animations** | Transitions, easing functions, durations, `@keyframes` |
|
|
56
|
+
| **Components** | Detected patterns for buttons, cards, inputs, links — with base styles |
|
|
57
|
+
|
|
58
|
+
## CLI Options
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
designlang <url> [options]
|
|
62
|
+
|
|
63
|
+
Options:
|
|
64
|
+
-o, --out <dir> Output directory (default: ./designlangtract-output)
|
|
65
|
+
-n, --name <name> Output file prefix (default: derived from URL hostname)
|
|
66
|
+
-w, --width <px> Viewport width (default: 1280)
|
|
67
|
+
-h, --height <px> Viewport height (default: 800)
|
|
68
|
+
--wait <ms> Wait after page load for SPAs (default: 0)
|
|
69
|
+
--dark Also extract dark mode color scheme
|
|
70
|
+
--verbose Show detailed extraction progress
|
|
71
|
+
-V, --version Show version
|
|
72
|
+
-h, --help Show help
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Examples
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Basic extraction
|
|
79
|
+
designlang https://vercel.com
|
|
80
|
+
|
|
81
|
+
# Custom output directory
|
|
82
|
+
designlang https://stripe.com --out ./stripe-design
|
|
83
|
+
|
|
84
|
+
# Extract dark mode too
|
|
85
|
+
designlang https://github.com --dark
|
|
86
|
+
|
|
87
|
+
# Wait for SPA to render
|
|
88
|
+
designlang https://app.example.com --wait 3000
|
|
89
|
+
|
|
90
|
+
# Custom viewport
|
|
91
|
+
designlang https://example.com --width 1440 --height 900
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## The Markdown Output
|
|
95
|
+
|
|
96
|
+
The `*-design-language.md` file is the hero output. It's structured for AI/LLM consumption with:
|
|
97
|
+
|
|
98
|
+
- Color palette tables with hex, RGB, and HSL values
|
|
99
|
+
- Typography scale with size, weight, line-height, and letter-spacing
|
|
100
|
+
- Spacing scale with token names and px/rem conversions
|
|
101
|
+
- CSS code blocks for shadows, component patterns, and animations
|
|
102
|
+
- A "Quick Start" section with step-by-step instructions to recreate the design
|
|
103
|
+
|
|
104
|
+
Feed it to any AI coding assistant and it can recreate the site's visual design from scratch.
|
|
105
|
+
|
|
106
|
+
## Claude Code Plugin
|
|
107
|
+
|
|
108
|
+
**designlang** also works as a [Claude Code](https://claude.ai/claude-code) plugin.
|
|
109
|
+
|
|
110
|
+
After installing, use the `/extract-design` slash command:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
/extract-design https://stripe.com
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Claude will extract the design, read the markdown output, and help you integrate it into your project.
|
|
117
|
+
|
|
118
|
+
## How It Works
|
|
119
|
+
|
|
120
|
+
1. **Crawl** — Launches headless Chromium via Playwright, navigates to the URL, waits for network idle and font loading
|
|
121
|
+
2. **Extract** — Runs a single `page.evaluate()` call that walks up to 5,000 DOM elements and collects computed styles, CSS custom properties, media queries, and keyframes
|
|
122
|
+
3. **Process** — Nine extractor modules parse, deduplicate, cluster, and classify the raw style data into a unified design object
|
|
123
|
+
4. **Format** — Four formatter modules generate the output files
|
|
124
|
+
|
|
125
|
+
## Limitations
|
|
126
|
+
|
|
127
|
+
- **Cross-origin stylesheets** — CSS loaded from CDNs may not be inspectable via `document.styleSheets` (CORS). Computed styles are still captured since `getComputedStyle()` sees the final resolved values.
|
|
128
|
+
- **Shadow DOM** — Elements inside closed shadow roots are not accessible. Open shadow roots are partially supported.
|
|
129
|
+
- **CSS-in-JS** — Styles injected at runtime (styled-components, Emotion) are captured via computed styles but not as raw CSS rules.
|
|
130
|
+
- **Element cap** — DOM traversal is capped at 5,000 elements to prevent hanging on very large pages.
|
|
131
|
+
|
|
132
|
+
## Tech Stack
|
|
133
|
+
|
|
134
|
+
- [Playwright](https://playwright.dev/) — headless browser automation
|
|
135
|
+
- [Commander](https://github.com/tj/commander.js/) — CLI framework
|
|
136
|
+
- [Chalk](https://github.com/chalk/chalk) + [Ora](https://github.com/sindresorhus/ora) — terminal styling
|
|
137
|
+
|
|
138
|
+
Zero external dependencies for color parsing, clustering, or CSS processing — all handled with ~200 lines of pure JS utilities.
|
|
139
|
+
|
|
140
|
+
## Contributing
|
|
141
|
+
|
|
142
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
143
|
+
|
|
144
|
+
1. Fork the repo
|
|
145
|
+
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
|
146
|
+
3. Make your changes
|
|
147
|
+
4. Test on a few websites (`node bin/designlangtract.js https://example.com`)
|
|
148
|
+
5. Commit and push
|
|
149
|
+
6. Open a pull request
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
[MIT](LICENSE) - Manavarya Singh
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
5
|
+
import { resolve, join } from 'path';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { extractDesignLanguage } from '../src/index.js';
|
|
9
|
+
import { formatMarkdown } from '../src/formatters/markdown.js';
|
|
10
|
+
import { formatTokens } from '../src/formatters/tokens.js';
|
|
11
|
+
import { formatTailwind } from '../src/formatters/tailwind.js';
|
|
12
|
+
import { formatCssVars } from '../src/formatters/css-vars.js';
|
|
13
|
+
import { nameFromUrl } from '../src/utils.js';
|
|
14
|
+
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name('designlang')
|
|
19
|
+
.description('Extract the complete design language from any website')
|
|
20
|
+
.version('1.0.0')
|
|
21
|
+
.argument('<url>', 'URL to extract design language from')
|
|
22
|
+
.option('-o, --out <dir>', 'output directory', './design-extract-output')
|
|
23
|
+
.option('-n, --name <name>', 'output file prefix (default: derived from URL)')
|
|
24
|
+
.option('-w, --width <px>', 'viewport width', parseInt, 1280)
|
|
25
|
+
.option('-h, --height <px>', 'viewport height', parseInt, 800)
|
|
26
|
+
.option('--wait <ms>', 'wait after page load (ms)', parseInt, 0)
|
|
27
|
+
.option('--dark', 'also extract dark mode styles')
|
|
28
|
+
.option('--verbose', 'show detailed progress')
|
|
29
|
+
.action(async (url, opts) => {
|
|
30
|
+
// Ensure URL has protocol
|
|
31
|
+
if (!url.startsWith('http')) url = `https://${url}`;
|
|
32
|
+
|
|
33
|
+
const prefix = opts.name || nameFromUrl(url);
|
|
34
|
+
const outDir = resolve(opts.out);
|
|
35
|
+
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(chalk.bold(' design-extract'));
|
|
38
|
+
console.log(chalk.gray(` ${url}`));
|
|
39
|
+
console.log('');
|
|
40
|
+
|
|
41
|
+
const spinner = ora('Launching browser...').start();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
spinner.text = 'Crawling page and extracting styles...';
|
|
45
|
+
const design = await extractDesignLanguage(url, {
|
|
46
|
+
width: opts.width,
|
|
47
|
+
height: opts.height,
|
|
48
|
+
wait: opts.wait,
|
|
49
|
+
dark: opts.dark,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
spinner.text = 'Generating output files...';
|
|
53
|
+
|
|
54
|
+
mkdirSync(outDir, { recursive: true });
|
|
55
|
+
|
|
56
|
+
const files = [
|
|
57
|
+
{ name: `${prefix}-design-language.md`, content: formatMarkdown(design), label: 'Markdown (AI-optimized)' },
|
|
58
|
+
{ name: `${prefix}-design-tokens.json`, content: formatTokens(design), label: 'Design Tokens (W3C)' },
|
|
59
|
+
{ name: `${prefix}-tailwind.config.js`, content: formatTailwind(design), label: 'Tailwind Config' },
|
|
60
|
+
{ name: `${prefix}-variables.css`, content: formatCssVars(design), label: 'CSS Variables' },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
writeFileSync(join(outDir, file.name), file.content, 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
spinner.succeed('Extraction complete!');
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log(chalk.bold(' Output files:'));
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const size = Buffer.byteLength(file.content);
|
|
72
|
+
const sizeStr = size > 1024 ? `${(size / 1024).toFixed(1)}KB` : `${size}B`;
|
|
73
|
+
console.log(` ${chalk.green('✓')} ${chalk.cyan(file.name)} ${chalk.gray(`(${sizeStr})`)} — ${file.label}`);
|
|
74
|
+
}
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log(chalk.gray(` Saved to ${outDir}`));
|
|
77
|
+
|
|
78
|
+
// Summary stats
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(chalk.bold(' Summary:'));
|
|
81
|
+
console.log(` ${chalk.gray('Colors:')} ${design.colors.all.length} unique colors`);
|
|
82
|
+
console.log(` ${chalk.gray('Fonts:')} ${design.typography.families.map(f => f.name).join(', ') || 'none detected'}`);
|
|
83
|
+
console.log(` ${chalk.gray('Spacing:')} ${design.spacing.scale.length} values${design.spacing.base ? ` (base: ${design.spacing.base}px)` : ''}`);
|
|
84
|
+
console.log(` ${chalk.gray('Shadows:')} ${design.shadows.values.length} unique shadows`);
|
|
85
|
+
console.log(` ${chalk.gray('Radii:')} ${design.borders.radii.length} unique values`);
|
|
86
|
+
console.log(` ${chalk.gray('Breakpoints:')} ${design.breakpoints.length} breakpoints`);
|
|
87
|
+
console.log(` ${chalk.gray('Components:')} ${Object.keys(design.components).length} patterns detected`);
|
|
88
|
+
console.log(` ${chalk.gray('CSS Vars:')} ${Object.values(design.variables).reduce((s, v) => s + Object.keys(v).length, 0)} custom properties`);
|
|
89
|
+
console.log('');
|
|
90
|
+
|
|
91
|
+
} catch (err) {
|
|
92
|
+
spinner.fail('Extraction failed');
|
|
93
|
+
if (err.message.includes('playwright')) {
|
|
94
|
+
console.error(chalk.red('\n Playwright is not installed.'));
|
|
95
|
+
console.error(chalk.gray(' Run: npx playwright install chromium\n'));
|
|
96
|
+
} else {
|
|
97
|
+
console.error(chalk.red(`\n ${err.message}\n`));
|
|
98
|
+
if (opts.verbose) console.error(err.stack);
|
|
99
|
+
}
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "designlang",
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"designlang": "./bin/design-extract.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "npx playwright install chromium --with-deps 2>/dev/null || npx playwright install chromium",
|
|
12
|
+
"start": "node bin/design-extract.js",
|
|
13
|
+
"test": "node --test tests/"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"playwright": "^1.42.0",
|
|
17
|
+
"commander": "^12.0.0",
|
|
18
|
+
"chalk": "^5.3.0",
|
|
19
|
+
"ora": "^8.0.0"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"design-system",
|
|
26
|
+
"design-tokens",
|
|
27
|
+
"design-language",
|
|
28
|
+
"css",
|
|
29
|
+
"tailwind",
|
|
30
|
+
"playwright",
|
|
31
|
+
"extraction",
|
|
32
|
+
"colors",
|
|
33
|
+
"typography",
|
|
34
|
+
"claude-code",
|
|
35
|
+
"plugin"
|
|
36
|
+
],
|
|
37
|
+
"author": "masyv",
|
|
38
|
+
"license": "MIT"
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: extract-design
|
|
3
|
+
description: "Extract the full design language from any website URL. Produces AI-optimized markdown, W3C design tokens, Tailwind config, and CSS variables. Use when user says 'extract design', 'get design system', 'design language', 'design tokens', 'what colors does this site use', 'what font does this site use', or '/extract-design'."
|
|
4
|
+
argument-hint: "<url> [--dark] [--out <dir>]"
|
|
5
|
+
allowed-tools: Bash, Read, Write, Glob
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Extract Design Language
|
|
9
|
+
|
|
10
|
+
Extract the complete design language from any website URL.
|
|
11
|
+
|
|
12
|
+
## Process
|
|
13
|
+
|
|
14
|
+
1. **Run the extraction CLI** on the provided URL:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd "${CLAUDE_SKILL_DIR}/../.." && node bin/design-extract.js $ARGUMENTS
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If dependencies are not installed, run first:
|
|
21
|
+
```bash
|
|
22
|
+
cd "${CLAUDE_SKILL_DIR}/../.." && npm install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
2. **Read the generated markdown file** to understand the design:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cat design-extract-output/*-design-language.md
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
3. **Present key findings** to the user:
|
|
32
|
+
- Primary color palette (with hex codes)
|
|
33
|
+
- Font families in use
|
|
34
|
+
- Spacing system (base unit if detected)
|
|
35
|
+
- Number of component patterns found
|
|
36
|
+
- Any notable design decisions (shadows, border-radius scale, etc.)
|
|
37
|
+
|
|
38
|
+
4. **Offer next steps:**
|
|
39
|
+
- Copy `tailwind.config.js` into the user's project
|
|
40
|
+
- Import `variables.css` into their stylesheet
|
|
41
|
+
- Use `design-tokens.json` for tooling integration
|
|
42
|
+
- Use the markdown file as a reference for AI-assisted development
|
|
43
|
+
|
|
44
|
+
## Output Files
|
|
45
|
+
|
|
46
|
+
The tool generates 4 files in the output directory:
|
|
47
|
+
|
|
48
|
+
| File | Purpose |
|
|
49
|
+
|------|---------|
|
|
50
|
+
| `*-design-language.md` | AI-optimized markdown describing the full design system |
|
|
51
|
+
| `*-design-tokens.json` | W3C Design Tokens format for tooling |
|
|
52
|
+
| `*-tailwind.config.js` | Ready-to-use Tailwind CSS theme extension |
|
|
53
|
+
| `*-variables.css` | CSS custom properties for direct use |
|
|
54
|
+
|
|
55
|
+
## Options
|
|
56
|
+
|
|
57
|
+
- `--out <dir>` — Output directory (default: `./design-extract-output`)
|
|
58
|
+
- `--dark` — Also extract dark mode color scheme
|
|
59
|
+
- `--wait <ms>` — Wait time after page load for SPAs
|
package/src/crawler.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
|
|
3
|
+
const MAX_ELEMENTS = 5000;
|
|
4
|
+
|
|
5
|
+
export async function crawlPage(url, options = {}) {
|
|
6
|
+
const { width = 1280, height = 800, wait = 0, dark = false } = options;
|
|
7
|
+
|
|
8
|
+
const browser = await chromium.launch({ headless: true });
|
|
9
|
+
const context = await browser.newContext({
|
|
10
|
+
viewport: { width, height },
|
|
11
|
+
colorScheme: 'light',
|
|
12
|
+
});
|
|
13
|
+
const page = await context.newPage();
|
|
14
|
+
|
|
15
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
|
|
16
|
+
if (wait > 0) await page.waitForTimeout(wait);
|
|
17
|
+
|
|
18
|
+
// Wait for fonts to load
|
|
19
|
+
await page.evaluate(() => document.fonts.ready);
|
|
20
|
+
|
|
21
|
+
const lightData = await extractPageData(page);
|
|
22
|
+
|
|
23
|
+
let darkData = null;
|
|
24
|
+
if (dark) {
|
|
25
|
+
await context.close();
|
|
26
|
+
const darkContext = await browser.newContext({
|
|
27
|
+
viewport: { width, height },
|
|
28
|
+
colorScheme: 'dark',
|
|
29
|
+
});
|
|
30
|
+
const darkPage = await darkContext.newPage();
|
|
31
|
+
await darkPage.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
|
|
32
|
+
await darkPage.evaluate(() => document.fonts.ready);
|
|
33
|
+
darkData = await extractPageData(darkPage);
|
|
34
|
+
await darkContext.close();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const title = await page.title();
|
|
38
|
+
await browser.close();
|
|
39
|
+
|
|
40
|
+
return { url, title, light: lightData, dark: darkData };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function extractPageData(page) {
|
|
44
|
+
return page.evaluate((maxElements) => {
|
|
45
|
+
const results = {
|
|
46
|
+
computedStyles: [],
|
|
47
|
+
cssVariables: {},
|
|
48
|
+
mediaQueries: [],
|
|
49
|
+
keyframes: [],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// 1. Walk all elements and collect computed styles
|
|
53
|
+
const allElements = document.querySelectorAll('*');
|
|
54
|
+
const elements = allElements.length > maxElements
|
|
55
|
+
? Array.from(allElements).slice(0, maxElements)
|
|
56
|
+
: Array.from(allElements);
|
|
57
|
+
|
|
58
|
+
for (const el of elements) {
|
|
59
|
+
const cs = getComputedStyle(el);
|
|
60
|
+
const tag = el.tagName.toLowerCase();
|
|
61
|
+
const classList = Array.from(el.classList).join(' ');
|
|
62
|
+
const role = el.getAttribute('role') || '';
|
|
63
|
+
|
|
64
|
+
// Get bounding rect for area estimation
|
|
65
|
+
const rect = el.getBoundingClientRect();
|
|
66
|
+
const area = rect.width * rect.height;
|
|
67
|
+
|
|
68
|
+
results.computedStyles.push({
|
|
69
|
+
tag,
|
|
70
|
+
classList,
|
|
71
|
+
role,
|
|
72
|
+
area,
|
|
73
|
+
color: cs.color,
|
|
74
|
+
backgroundColor: cs.backgroundColor,
|
|
75
|
+
backgroundImage: cs.backgroundImage,
|
|
76
|
+
borderColor: cs.borderColor,
|
|
77
|
+
fontFamily: cs.fontFamily,
|
|
78
|
+
fontSize: cs.fontSize,
|
|
79
|
+
fontWeight: cs.fontWeight,
|
|
80
|
+
lineHeight: cs.lineHeight,
|
|
81
|
+
letterSpacing: cs.letterSpacing,
|
|
82
|
+
paddingTop: cs.paddingTop,
|
|
83
|
+
paddingRight: cs.paddingRight,
|
|
84
|
+
paddingBottom: cs.paddingBottom,
|
|
85
|
+
paddingLeft: cs.paddingLeft,
|
|
86
|
+
marginTop: cs.marginTop,
|
|
87
|
+
marginRight: cs.marginRight,
|
|
88
|
+
marginBottom: cs.marginBottom,
|
|
89
|
+
marginLeft: cs.marginLeft,
|
|
90
|
+
gap: cs.gap,
|
|
91
|
+
borderRadius: cs.borderRadius,
|
|
92
|
+
boxShadow: cs.boxShadow,
|
|
93
|
+
zIndex: cs.zIndex,
|
|
94
|
+
transition: cs.transition,
|
|
95
|
+
animation: cs.animation,
|
|
96
|
+
display: cs.display,
|
|
97
|
+
position: cs.position,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Extract CSS custom properties from :root
|
|
102
|
+
const rootStyles = getComputedStyle(document.documentElement);
|
|
103
|
+
// Get all custom properties by iterating stylesheets
|
|
104
|
+
try {
|
|
105
|
+
for (const sheet of document.styleSheets) {
|
|
106
|
+
try {
|
|
107
|
+
for (const rule of sheet.cssRules) {
|
|
108
|
+
if (rule.selectorText === ':root' || rule.selectorText === ':host') {
|
|
109
|
+
for (let i = 0; i < rule.style.length; i++) {
|
|
110
|
+
const prop = rule.style[i];
|
|
111
|
+
if (prop.startsWith('--')) {
|
|
112
|
+
results.cssVariables[prop] = rule.style.getPropertyValue(prop).trim();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch { /* cross-origin stylesheet, skip */ }
|
|
118
|
+
}
|
|
119
|
+
} catch { /* no stylesheets accessible */ }
|
|
120
|
+
|
|
121
|
+
// Also get any custom properties from the computed style
|
|
122
|
+
// (fallback for CSS-in-JS that sets vars on :root)
|
|
123
|
+
for (let i = 0; i < rootStyles.length; i++) {
|
|
124
|
+
const prop = rootStyles[i];
|
|
125
|
+
if (prop.startsWith('--') && !results.cssVariables[prop]) {
|
|
126
|
+
results.cssVariables[prop] = rootStyles.getPropertyValue(prop).trim();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 3. Extract media queries from stylesheets
|
|
131
|
+
try {
|
|
132
|
+
for (const sheet of document.styleSheets) {
|
|
133
|
+
try {
|
|
134
|
+
for (const rule of sheet.cssRules) {
|
|
135
|
+
if (rule instanceof CSSMediaRule) {
|
|
136
|
+
results.mediaQueries.push(rule.conditionText || rule.media.mediaText);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch { /* cross-origin */ }
|
|
140
|
+
}
|
|
141
|
+
} catch { /* no access */ }
|
|
142
|
+
|
|
143
|
+
// 4. Extract keyframes
|
|
144
|
+
try {
|
|
145
|
+
for (const sheet of document.styleSheets) {
|
|
146
|
+
try {
|
|
147
|
+
for (const rule of sheet.cssRules) {
|
|
148
|
+
if (rule instanceof CSSKeyframesRule) {
|
|
149
|
+
const steps = [];
|
|
150
|
+
for (const kf of rule.cssRules) {
|
|
151
|
+
steps.push({ offset: kf.keyText, style: kf.style.cssText });
|
|
152
|
+
}
|
|
153
|
+
results.keyframes.push({ name: rule.name, steps });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch { /* cross-origin */ }
|
|
157
|
+
}
|
|
158
|
+
} catch { /* no access */ }
|
|
159
|
+
|
|
160
|
+
return results;
|
|
161
|
+
}, MAX_ELEMENTS);
|
|
162
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function extractAnimations(computedStyles, keyframes) {
|
|
2
|
+
const transitionSet = new Set();
|
|
3
|
+
const easingSet = new Set();
|
|
4
|
+
const durationSet = new Set();
|
|
5
|
+
|
|
6
|
+
for (const el of computedStyles) {
|
|
7
|
+
if (el.transition && el.transition !== 'all 0s ease 0s' && el.transition !== 'none') {
|
|
8
|
+
transitionSet.add(el.transition);
|
|
9
|
+
|
|
10
|
+
// Extract easing and duration
|
|
11
|
+
const dMatch = el.transition.match(/([\d.]+m?s)/g);
|
|
12
|
+
if (dMatch) dMatch.forEach(d => durationSet.add(d));
|
|
13
|
+
|
|
14
|
+
const eMatch = el.transition.match(/(ease|ease-in|ease-out|ease-in-out|linear|cubic-bezier\([^)]+\))/g);
|
|
15
|
+
if (eMatch) eMatch.forEach(e => easingSet.add(e));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
transitions: [...transitionSet],
|
|
21
|
+
easings: [...easingSet],
|
|
22
|
+
durations: [...durationSet],
|
|
23
|
+
keyframes: keyframes.map(kf => ({
|
|
24
|
+
name: kf.name,
|
|
25
|
+
steps: kf.steps,
|
|
26
|
+
})),
|
|
27
|
+
};
|
|
28
|
+
}
|