@elyracode/design-lookup 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/extensions/index.ts +436 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @elyracode/design-lookup
|
|
2
|
+
|
|
3
|
+
Extract design systems from any website. Colors, typography, spacing, components, and layout patterns -- saved as a structured markdown file the agent can reference.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
elyra install npm:@elyracode/design-lookup
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
Puppeteer must be installed globally:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
npm install -g puppeteer
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Command
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
/lookup-design https://stripe.com
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Extracts the design system and saves it to `.elyra/LOOKUPDESIGN.md`.
|
|
28
|
+
|
|
29
|
+
### Tool
|
|
30
|
+
|
|
31
|
+
The agent can also call `lookup_design` automatically when you ask it to match a website's style:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
> Build a landing page that matches the design of stripe.com
|
|
35
|
+
> Make our checkout look like shopify.com
|
|
36
|
+
> Use the same color palette as linear.app
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## What it extracts
|
|
40
|
+
|
|
41
|
+
- **Colors**: Primary, secondary, background, text, accent, and all CSS custom properties
|
|
42
|
+
- **Typography**: Font families, weights, sizes, line heights
|
|
43
|
+
- **Spacing**: Spacing scale derived from actual element measurements
|
|
44
|
+
- **Components**: Button styles, card patterns, input styles, navigation
|
|
45
|
+
- **Layout**: Max widths, grid systems, breakpoints
|
|
46
|
+
|
|
47
|
+
## Output
|
|
48
|
+
|
|
49
|
+
Results are saved to `.elyra/LOOKUPDESIGN.md` in a structured format the agent can parse and apply. The file is overwritten on each lookup.
|
|
50
|
+
|
|
51
|
+
Pin it for persistent reference:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
/pin .elyra/LOOKUPDESIGN.md
|
|
55
|
+
```
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ExtensionAPI } from "@elyracode/coding-agent";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
|
|
7
|
+
const EXTRACTION_SCRIPT = `
|
|
8
|
+
(function() {
|
|
9
|
+
const result = { colors: {}, typography: {}, spacing: [], components: {}, layout: {}, cssVars: {} };
|
|
10
|
+
|
|
11
|
+
// Extract CSS custom properties from :root
|
|
12
|
+
try {
|
|
13
|
+
const rootStyles = getComputedStyle(document.documentElement);
|
|
14
|
+
const sheets = document.styleSheets;
|
|
15
|
+
for (const sheet of sheets) {
|
|
16
|
+
try {
|
|
17
|
+
for (const rule of sheet.cssRules) {
|
|
18
|
+
if (rule.selectorText === ':root' || rule.selectorText === ':root, :host') {
|
|
19
|
+
const style = rule.style;
|
|
20
|
+
for (let i = 0; i < style.length; i++) {
|
|
21
|
+
const prop = style[i];
|
|
22
|
+
if (prop.startsWith('--')) {
|
|
23
|
+
const value = style.getPropertyValue(prop).trim();
|
|
24
|
+
if (value) result.cssVars[prop] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch(e) { /* cross-origin stylesheet */ }
|
|
30
|
+
}
|
|
31
|
+
} catch(e) {}
|
|
32
|
+
|
|
33
|
+
// Extract colors from key elements
|
|
34
|
+
const colorMap = new Map();
|
|
35
|
+
function addColor(label, el) {
|
|
36
|
+
const s = getComputedStyle(el);
|
|
37
|
+
const bg = s.backgroundColor;
|
|
38
|
+
const fg = s.color;
|
|
39
|
+
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') colorMap.set(label + '-bg', bg);
|
|
40
|
+
if (fg) colorMap.set(label + '-text', fg);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const body = document.body;
|
|
44
|
+
if (body) addColor('body', body);
|
|
45
|
+
|
|
46
|
+
const nav = document.querySelector('nav, header, [role="navigation"]');
|
|
47
|
+
if (nav) addColor('nav', nav);
|
|
48
|
+
|
|
49
|
+
const main = document.querySelector('main, [role="main"], .main, #main');
|
|
50
|
+
if (main) addColor('main', main);
|
|
51
|
+
|
|
52
|
+
const footer = document.querySelector('footer, [role="contentinfo"]');
|
|
53
|
+
if (footer) addColor('footer', footer);
|
|
54
|
+
|
|
55
|
+
// Primary button
|
|
56
|
+
const btn = document.querySelector('button, [role="button"], a.btn, a.button, .btn-primary, [class*="btn"]');
|
|
57
|
+
if (btn) addColor('button', btn);
|
|
58
|
+
|
|
59
|
+
// Links
|
|
60
|
+
const link = document.querySelector('a[href]:not(nav a):not(header a)');
|
|
61
|
+
if (link) addColor('link', link);
|
|
62
|
+
|
|
63
|
+
// Headings
|
|
64
|
+
const h1 = document.querySelector('h1');
|
|
65
|
+
if (h1) addColor('heading', h1);
|
|
66
|
+
|
|
67
|
+
result.colors = Object.fromEntries(colorMap);
|
|
68
|
+
|
|
69
|
+
// Extract typography
|
|
70
|
+
const fontFamilies = new Set();
|
|
71
|
+
const fontSizes = new Set();
|
|
72
|
+
const fontWeights = new Set();
|
|
73
|
+
const lineHeights = new Set();
|
|
74
|
+
|
|
75
|
+
const textEls = document.querySelectorAll('h1, h2, h3, h4, p, span, a, li, td, th, label, button');
|
|
76
|
+
for (const el of Array.from(textEls).slice(0, 50)) {
|
|
77
|
+
const s = getComputedStyle(el);
|
|
78
|
+
fontFamilies.add(s.fontFamily.split(',')[0].trim().replace(/['"]/g, ''));
|
|
79
|
+
fontSizes.add(s.fontSize);
|
|
80
|
+
fontWeights.add(s.fontWeight);
|
|
81
|
+
lineHeights.add(s.lineHeight);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
result.typography = {
|
|
85
|
+
families: [...fontFamilies].slice(0, 5),
|
|
86
|
+
sizes: [...fontSizes].sort((a, b) => parseFloat(a) - parseFloat(b)),
|
|
87
|
+
weights: [...fontWeights].sort(),
|
|
88
|
+
lineHeights: [...new Set([...lineHeights].filter(lh => lh !== 'normal'))].sort()
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Extract heading styles
|
|
92
|
+
const headingStyles = {};
|
|
93
|
+
for (const tag of ['h1', 'h2', 'h3']) {
|
|
94
|
+
const el = document.querySelector(tag);
|
|
95
|
+
if (el) {
|
|
96
|
+
const s = getComputedStyle(el);
|
|
97
|
+
headingStyles[tag] = {
|
|
98
|
+
fontSize: s.fontSize,
|
|
99
|
+
fontWeight: s.fontWeight,
|
|
100
|
+
fontFamily: s.fontFamily.split(',')[0].trim().replace(/['"]/g, ''),
|
|
101
|
+
lineHeight: s.lineHeight,
|
|
102
|
+
color: s.color
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
result.typography.headings = headingStyles;
|
|
107
|
+
|
|
108
|
+
// Body text
|
|
109
|
+
const bodyText = document.querySelector('p');
|
|
110
|
+
if (bodyText) {
|
|
111
|
+
const s = getComputedStyle(bodyText);
|
|
112
|
+
result.typography.body = {
|
|
113
|
+
fontSize: s.fontSize,
|
|
114
|
+
fontWeight: s.fontWeight,
|
|
115
|
+
fontFamily: s.fontFamily.split(',')[0].trim().replace(/['"]/g, ''),
|
|
116
|
+
lineHeight: s.lineHeight,
|
|
117
|
+
color: s.color
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extract spacing patterns
|
|
122
|
+
const spacingValues = new Set();
|
|
123
|
+
const spacingEls = document.querySelectorAll('div, section, article, main, header, footer, p, h1, h2, h3, ul, ol');
|
|
124
|
+
for (const el of Array.from(spacingEls).slice(0, 40)) {
|
|
125
|
+
const s = getComputedStyle(el);
|
|
126
|
+
for (const prop of ['marginTop', 'marginBottom', 'marginLeft', 'marginRight', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', 'gap']) {
|
|
127
|
+
const val = parseFloat(s[prop]);
|
|
128
|
+
if (val > 0 && val < 200) spacingValues.add(Math.round(val));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
result.spacing = [...spacingValues].sort((a, b) => a - b);
|
|
132
|
+
|
|
133
|
+
// Extract component styles
|
|
134
|
+
// Buttons
|
|
135
|
+
const buttons = document.querySelectorAll('button, [role="button"], a.btn, .btn, [class*="button"]');
|
|
136
|
+
if (buttons.length > 0) {
|
|
137
|
+
const s = getComputedStyle(buttons[0]);
|
|
138
|
+
result.components.button = {
|
|
139
|
+
backgroundColor: s.backgroundColor,
|
|
140
|
+
color: s.color,
|
|
141
|
+
borderRadius: s.borderRadius,
|
|
142
|
+
padding: s.padding,
|
|
143
|
+
fontSize: s.fontSize,
|
|
144
|
+
fontWeight: s.fontWeight,
|
|
145
|
+
border: s.border
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Cards
|
|
150
|
+
const card = document.querySelector('[class*="card"], [class*="Card"], .panel, [class*="tile"]');
|
|
151
|
+
if (card) {
|
|
152
|
+
const s = getComputedStyle(card);
|
|
153
|
+
result.components.card = {
|
|
154
|
+
backgroundColor: s.backgroundColor,
|
|
155
|
+
borderRadius: s.borderRadius,
|
|
156
|
+
padding: s.padding,
|
|
157
|
+
boxShadow: s.boxShadow !== 'none' ? s.boxShadow : undefined,
|
|
158
|
+
border: s.border !== 'none' ? s.border : undefined
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Inputs
|
|
163
|
+
const input = document.querySelector('input[type="text"], input[type="email"], input:not([type]), textarea');
|
|
164
|
+
if (input) {
|
|
165
|
+
const s = getComputedStyle(input);
|
|
166
|
+
result.components.input = {
|
|
167
|
+
backgroundColor: s.backgroundColor,
|
|
168
|
+
borderRadius: s.borderRadius,
|
|
169
|
+
padding: s.padding,
|
|
170
|
+
border: s.border,
|
|
171
|
+
fontSize: s.fontSize,
|
|
172
|
+
color: s.color
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Layout
|
|
177
|
+
const containers = document.querySelectorAll('[class*="container"], [class*="Container"], [class*="wrapper"], main, .max-w');
|
|
178
|
+
let maxWidth = 'none';
|
|
179
|
+
for (const el of containers) {
|
|
180
|
+
const s = getComputedStyle(el);
|
|
181
|
+
const mw = s.maxWidth;
|
|
182
|
+
if (mw && mw !== 'none' && mw !== '100%') {
|
|
183
|
+
maxWidth = mw;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
result.layout.maxWidth = maxWidth;
|
|
188
|
+
|
|
189
|
+
// Detect grid usage
|
|
190
|
+
const gridEls = document.querySelectorAll('[style*="grid"], [class*="grid"], [class*="Grid"]');
|
|
191
|
+
result.layout.usesGrid = gridEls.length > 0;
|
|
192
|
+
|
|
193
|
+
const flexEls = document.querySelectorAll('[style*="flex"], [class*="flex"], [class*="Flex"]');
|
|
194
|
+
result.layout.usesFlex = flexEls.length > 0;
|
|
195
|
+
|
|
196
|
+
return JSON.stringify(result);
|
|
197
|
+
})()
|
|
198
|
+
`;
|
|
199
|
+
|
|
200
|
+
function rgbToHex(rgb: string): string {
|
|
201
|
+
const match = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
202
|
+
if (!match) return rgb;
|
|
203
|
+
const r = parseInt(match[1]);
|
|
204
|
+
const g = parseInt(match[2]);
|
|
205
|
+
const b = parseInt(match[3]);
|
|
206
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatDesignSystem(url: string, data: any): string {
|
|
210
|
+
const lines: string[] = [];
|
|
211
|
+
lines.push(`# Design System: ${url}`);
|
|
212
|
+
lines.push(``);
|
|
213
|
+
lines.push(`> Extracted ${new Date().toISOString().split("T")[0]}`);
|
|
214
|
+
lines.push(``);
|
|
215
|
+
|
|
216
|
+
// Colors
|
|
217
|
+
lines.push(`## Colors`);
|
|
218
|
+
lines.push(``);
|
|
219
|
+
if (data.colors && Object.keys(data.colors).length > 0) {
|
|
220
|
+
for (const [key, value] of Object.entries(data.colors)) {
|
|
221
|
+
const hex = rgbToHex(value as string);
|
|
222
|
+
lines.push(`- **${key}**: ${hex}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// CSS Custom Properties (color-related)
|
|
227
|
+
if (data.cssVars && Object.keys(data.cssVars).length > 0) {
|
|
228
|
+
const colorVars = Object.entries(data.cssVars).filter(
|
|
229
|
+
([k, v]) => typeof v === "string" && (
|
|
230
|
+
(v as string).startsWith("#") ||
|
|
231
|
+
(v as string).startsWith("rgb") ||
|
|
232
|
+
(v as string).startsWith("hsl") ||
|
|
233
|
+
k.includes("color") || k.includes("bg") || k.includes("foreground") || k.includes("background")
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
if (colorVars.length > 0) {
|
|
237
|
+
lines.push(``);
|
|
238
|
+
lines.push(`### CSS Variables`);
|
|
239
|
+
for (const [key, value] of colorVars.slice(0, 20)) {
|
|
240
|
+
lines.push(`- \`${key}\`: ${value}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
lines.push(``);
|
|
245
|
+
|
|
246
|
+
// Typography
|
|
247
|
+
lines.push(`## Typography`);
|
|
248
|
+
lines.push(``);
|
|
249
|
+
if (data.typography) {
|
|
250
|
+
if (data.typography.families?.length > 0) {
|
|
251
|
+
lines.push(`### Font Families`);
|
|
252
|
+
for (const f of data.typography.families) {
|
|
253
|
+
lines.push(`- ${f}`);
|
|
254
|
+
}
|
|
255
|
+
lines.push(``);
|
|
256
|
+
}
|
|
257
|
+
if (data.typography.body) {
|
|
258
|
+
const b = data.typography.body;
|
|
259
|
+
lines.push(`### Body Text`);
|
|
260
|
+
lines.push(`- Font: ${b.fontFamily}`);
|
|
261
|
+
lines.push(`- Size: ${b.fontSize}`);
|
|
262
|
+
lines.push(`- Weight: ${b.fontWeight}`);
|
|
263
|
+
lines.push(`- Line height: ${b.lineHeight}`);
|
|
264
|
+
lines.push(``);
|
|
265
|
+
}
|
|
266
|
+
if (data.typography.headings && Object.keys(data.typography.headings).length > 0) {
|
|
267
|
+
lines.push(`### Headings`);
|
|
268
|
+
for (const [tag, style] of Object.entries(data.typography.headings)) {
|
|
269
|
+
const s = style as any;
|
|
270
|
+
lines.push(`- **${tag}**: ${s.fontSize}, weight ${s.fontWeight}, ${s.fontFamily}`);
|
|
271
|
+
}
|
|
272
|
+
lines.push(``);
|
|
273
|
+
}
|
|
274
|
+
if (data.typography.sizes?.length > 0) {
|
|
275
|
+
lines.push(`### Size Scale`);
|
|
276
|
+
lines.push(`${data.typography.sizes.join(", ")}`);
|
|
277
|
+
lines.push(``);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Spacing
|
|
282
|
+
lines.push(`## Spacing`);
|
|
283
|
+
lines.push(``);
|
|
284
|
+
if (data.spacing?.length > 0) {
|
|
285
|
+
lines.push(`Scale (px): ${data.spacing.join(", ")}`);
|
|
286
|
+
const base = data.spacing.length > 1 ? data.spacing[1] - data.spacing[0] : data.spacing[0];
|
|
287
|
+
if (base === 4 || base === 8) {
|
|
288
|
+
lines.push(`Base unit: ${base}px`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
lines.push(``);
|
|
292
|
+
|
|
293
|
+
// Components
|
|
294
|
+
lines.push(`## Components`);
|
|
295
|
+
lines.push(``);
|
|
296
|
+
if (data.components) {
|
|
297
|
+
if (data.components.button) {
|
|
298
|
+
const b = data.components.button;
|
|
299
|
+
lines.push(`### Button`);
|
|
300
|
+
lines.push(`- Background: ${rgbToHex(b.backgroundColor)}`);
|
|
301
|
+
lines.push(`- Text: ${rgbToHex(b.color)}`);
|
|
302
|
+
lines.push(`- Border radius: ${b.borderRadius}`);
|
|
303
|
+
lines.push(`- Padding: ${b.padding}`);
|
|
304
|
+
lines.push(`- Font: ${b.fontSize}, weight ${b.fontWeight}`);
|
|
305
|
+
lines.push(``);
|
|
306
|
+
}
|
|
307
|
+
if (data.components.card) {
|
|
308
|
+
const c = data.components.card;
|
|
309
|
+
lines.push(`### Card`);
|
|
310
|
+
lines.push(`- Background: ${rgbToHex(c.backgroundColor)}`);
|
|
311
|
+
lines.push(`- Border radius: ${c.borderRadius}`);
|
|
312
|
+
lines.push(`- Padding: ${c.padding}`);
|
|
313
|
+
if (c.boxShadow) lines.push(`- Shadow: ${c.boxShadow}`);
|
|
314
|
+
if (c.border) lines.push(`- Border: ${c.border}`);
|
|
315
|
+
lines.push(``);
|
|
316
|
+
}
|
|
317
|
+
if (data.components.input) {
|
|
318
|
+
const i = data.components.input;
|
|
319
|
+
lines.push(`### Input`);
|
|
320
|
+
lines.push(`- Background: ${rgbToHex(i.backgroundColor)}`);
|
|
321
|
+
lines.push(`- Border: ${i.border}`);
|
|
322
|
+
lines.push(`- Border radius: ${i.borderRadius}`);
|
|
323
|
+
lines.push(`- Padding: ${i.padding}`);
|
|
324
|
+
lines.push(`- Font size: ${i.fontSize}`);
|
|
325
|
+
lines.push(``);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Layout
|
|
330
|
+
lines.push(`## Layout`);
|
|
331
|
+
lines.push(``);
|
|
332
|
+
if (data.layout) {
|
|
333
|
+
if (data.layout.maxWidth !== "none") lines.push(`- Max width: ${data.layout.maxWidth}`);
|
|
334
|
+
if (data.layout.usesGrid) lines.push(`- Uses CSS Grid`);
|
|
335
|
+
if (data.layout.usesFlex) lines.push(`- Uses Flexbox`);
|
|
336
|
+
}
|
|
337
|
+
lines.push(``);
|
|
338
|
+
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export default function (elyra: ExtensionAPI): void {
|
|
343
|
+
elyra.registerTool({
|
|
344
|
+
name: "lookup_design",
|
|
345
|
+
label: "Lookup Design System",
|
|
346
|
+
description:
|
|
347
|
+
"Extract the design system from a website URL. Returns colors, typography, spacing, " +
|
|
348
|
+
"component styles, and layout patterns. Saves results to .elyra/LOOKUPDESIGN.md. " +
|
|
349
|
+
"Use when asked to match, replicate, or reference another website's design. " +
|
|
350
|
+
"Requires Puppeteer (npm install -g puppeteer).",
|
|
351
|
+
parameters: Type.Object({
|
|
352
|
+
url: Type.String({
|
|
353
|
+
description: "Website URL to extract design from (e.g., https://stripe.com)",
|
|
354
|
+
}),
|
|
355
|
+
}),
|
|
356
|
+
execute: async (_toolCallId, params) => {
|
|
357
|
+
const cwd = process.cwd();
|
|
358
|
+
const elyraDir = join(cwd, ".elyra");
|
|
359
|
+
const outputPath = join(elyraDir, "LOOKUPDESIGN.md");
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
if (!existsSync(elyraDir)) {
|
|
363
|
+
mkdirSync(elyraDir, { recursive: true });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Write extraction script to temp file
|
|
367
|
+
const tmpDir = join(require("os").tmpdir(), "elyra-design-lookup");
|
|
368
|
+
if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true });
|
|
369
|
+
|
|
370
|
+
const scriptPath = join(tmpDir, "extract.mjs");
|
|
371
|
+
const puppeteerScript = `
|
|
372
|
+
import puppeteer from 'puppeteer';
|
|
373
|
+
|
|
374
|
+
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });
|
|
375
|
+
const page = await browser.newPage();
|
|
376
|
+
await page.setViewport({ width: 1280, height: 800 });
|
|
377
|
+
await page.goto('${params.url}', { waitUntil: 'networkidle2', timeout: 20000 });
|
|
378
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
379
|
+
const result = await page.evaluate(() => {
|
|
380
|
+
${EXTRACTION_SCRIPT}
|
|
381
|
+
});
|
|
382
|
+
console.log(result);
|
|
383
|
+
await browser.close();
|
|
384
|
+
`;
|
|
385
|
+
|
|
386
|
+
writeFileSync(scriptPath, puppeteerScript, "utf-8");
|
|
387
|
+
|
|
388
|
+
const rawResult = execSync(`node "${scriptPath}"`, {
|
|
389
|
+
timeout: 45000,
|
|
390
|
+
encoding: "utf-8",
|
|
391
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
392
|
+
}).trim();
|
|
393
|
+
|
|
394
|
+
const data = JSON.parse(rawResult);
|
|
395
|
+
const markdown = formatDesignSystem(params.url, data);
|
|
396
|
+
|
|
397
|
+
writeFileSync(outputPath, markdown, "utf-8");
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
content: [
|
|
401
|
+
{ type: "text", text: markdown },
|
|
402
|
+
{ type: "text", text: `\n\nSaved to ${outputPath}` },
|
|
403
|
+
],
|
|
404
|
+
details: { url: params.url, outputPath },
|
|
405
|
+
};
|
|
406
|
+
} catch (error) {
|
|
407
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
408
|
+
if (msg.includes("puppeteer") || msg.includes("Cannot find module")) {
|
|
409
|
+
return {
|
|
410
|
+
content: [{
|
|
411
|
+
type: "text",
|
|
412
|
+
text: "Puppeteer is required for design extraction.\n\nInstall: npm install -g puppeteer",
|
|
413
|
+
}],
|
|
414
|
+
details: { url: params.url, outputPath },
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
content: [{ type: "text", text: `Design extraction failed: ${msg}` }],
|
|
419
|
+
details: { url: params.url, outputPath },
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
elyra.registerCommand("lookup-design", {
|
|
426
|
+
description: "Extract design system from a website: /lookup-design <url>",
|
|
427
|
+
handler: async (args, ctx) => {
|
|
428
|
+
const url = args.trim();
|
|
429
|
+
if (!url) {
|
|
430
|
+
ctx.ui.notify("Usage: /lookup-design <url>\n\nExample: /lookup-design https://stripe.com", "error");
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
elyra.sendUserMessage(`Extract the design system from ${url} and save it to .elyra/LOOKUPDESIGN.md`);
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elyracode/design-lookup",
|
|
3
|
+
"version": "0.7.2",
|
|
4
|
+
"description": "Extract design systems from any website -- colors, typography, spacing, components, and layout patterns",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"elyra-package",
|
|
8
|
+
"design",
|
|
9
|
+
"design-system",
|
|
10
|
+
"reverse-engineering",
|
|
11
|
+
"css",
|
|
12
|
+
"tailwind"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Knut W. Horne",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/kwhorne/elyra.git",
|
|
19
|
+
"directory": "packages/design-lookup"
|
|
20
|
+
},
|
|
21
|
+
"elyra": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./extensions/index.ts"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@elyracode/coding-agent": "*",
|
|
28
|
+
"typebox": "*"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"clean": "echo 'nothing to clean'",
|
|
32
|
+
"build": "echo 'nothing to build'",
|
|
33
|
+
"check": "echo 'nothing to check'"
|
|
34
|
+
}
|
|
35
|
+
}
|