@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 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
+ }