@elyracode/design-tools 0.4.7

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-tools
2
+
3
+ Design tools for Elyra -- live browser preview, screenshot capture, and Tailwind design system checks.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ elyra install npm:@elyracode/design-tools
9
+ ```
10
+
11
+ ## Tools
12
+
13
+ | Tool | Description |
14
+ |------|-------------|
15
+ | `design_preview` | Render HTML/Tailwind in the browser with auto-reload. Iterate on design in real-time. |
16
+ | `screenshot_url` | Capture a screenshot of any URL (localhost or web). Returns image for visual QA. |
17
+ | `design_system_check` | Analyze files for Tailwind consistency: spacing, colors, responsive, dark mode, accessibility. |
18
+
19
+ ## Commands
20
+
21
+ - `/design` -- Interactive selector for all design tools
22
+
23
+ ## Usage
24
+
25
+ ### Live Preview
26
+ ```
27
+ > Preview a pricing table with 3 tiers using Tailwind
28
+ > Show me a hero section with gradient background
29
+ > Preview this component in dark mode
30
+ ```
31
+
32
+ The agent writes HTML with Tailwind classes, opens it in your browser, and the page auto-reloads every 2 seconds as you iterate.
33
+
34
+ ### Visual QA
35
+ ```
36
+ > Take a screenshot of localhost:8000 and check the layout
37
+ > Screenshot the login page on mobile viewport
38
+ > Capture the dashboard and evaluate the spacing
39
+ ```
40
+
41
+ The agent captures a screenshot and analyzes it with vision capabilities to spot layout issues, alignment problems, and responsive breakpoints.
42
+
43
+ ### Design System Check
44
+ ```
45
+ > Check resources/views/dashboard.blade.php for design consistency
46
+ > Analyze the spacing and colors in my pricing component
47
+ ```
48
+
49
+ Checks for: mixed spacing scales, missing responsive breakpoints, no hover/focus states, hardcoded colors, missing dark mode support, and conflicting classes.
50
+
51
+ ## Requirements
52
+
53
+ - **Preview**: No dependencies (uses Tailwind CDN)
54
+ - **Screenshots**: Install Puppeteer globally for automated captures: `npm install -g puppeteer`
55
+ - **Design Check**: No dependencies
@@ -0,0 +1,480 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { ExtensionAPI } from "@elyracode/coding-agent";
6
+ import { Type } from "typebox";
7
+
8
+ const PREVIEW_DIR = join(homedir(), ".elyra", "design-preview");
9
+
10
+ export default function (elyra: ExtensionAPI): void {
11
+ // ── Tool 1: design_preview ──
12
+ // Render a component in the browser with live Tailwind CSS
13
+ elyra.registerTool({
14
+ name: "design_preview",
15
+ label: "Design Preview",
16
+ description:
17
+ "Render HTML/Tailwind CSS code in the browser for live preview. " +
18
+ "Writes an HTML file with Tailwind CDN and opens it in the default browser. " +
19
+ "Use this to preview UI components, layouts, and pages. " +
20
+ "The user can see the result visually while you iterate on the code.",
21
+ parameters: Type.Object({
22
+ html: Type.String({
23
+ description:
24
+ "The HTML content to preview (can include Tailwind classes, inline styles, etc.)",
25
+ }),
26
+ title: Type.Optional(
27
+ Type.String({
28
+ description: "Page title (default: 'Elyra Design Preview')",
29
+ }),
30
+ ),
31
+ dark: Type.Optional(
32
+ Type.Boolean({
33
+ description: "Enable dark mode background (default: false)",
34
+ }),
35
+ ),
36
+ width: Type.Optional(
37
+ Type.String({
38
+ description:
39
+ "Container max-width (e.g., '800px', '100%'). Default: '1200px'",
40
+ }),
41
+ ),
42
+ }),
43
+ execute: async (_toolCallId, params) => {
44
+ try {
45
+ if (!existsSync(PREVIEW_DIR)) {
46
+ mkdirSync(PREVIEW_DIR, { recursive: true });
47
+ }
48
+
49
+ const title = params.title ?? "Elyra Design Preview";
50
+ const dark = params.dark ?? false;
51
+ const width = params.width ?? "1200px";
52
+ const bgClass = dark
53
+ ? "bg-gray-900 text-white"
54
+ : "bg-white text-gray-900";
55
+
56
+ const fullHtml = `<!DOCTYPE html>
57
+ <html lang="en" ${dark ? 'class="dark"' : ""}>
58
+ <head>
59
+ <meta charset="UTF-8">
60
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
61
+ <title>${title}</title>
62
+ <script src="https://cdn.tailwindcss.com"></script>
63
+ <script>
64
+ tailwind.config = {
65
+ darkMode: 'class',
66
+ theme: { extend: {} }
67
+ }
68
+ </script>
69
+ <style>
70
+ /* Smooth transitions for iterative design */
71
+ * { transition: all 0.15s ease; }
72
+ </style>
73
+ </head>
74
+ <body class="${bgClass} min-h-screen p-8">
75
+ <div style="max-width: ${width}; margin: 0 auto;">
76
+ ${params.html}
77
+ </div>
78
+ <script>
79
+ // Auto-reload every 2 seconds for live preview
80
+ setTimeout(() => location.reload(), 2000);
81
+ </script>
82
+ </body>
83
+ </html>`;
84
+
85
+ const previewPath = join(PREVIEW_DIR, "preview.html");
86
+ writeFileSync(previewPath, fullHtml, "utf-8");
87
+
88
+ // Open in default browser
89
+ const openCmd =
90
+ process.platform === "darwin"
91
+ ? "open"
92
+ : process.platform === "win32"
93
+ ? "start"
94
+ : "xdg-open";
95
+
96
+ try {
97
+ execSync(`${openCmd} "${previewPath}"`, { timeout: 5000 });
98
+ } catch {
99
+ // Browser might already be open on this file
100
+ }
101
+
102
+ return {
103
+ content: [
104
+ {
105
+ type: "text",
106
+ text: `Preview opened at ${previewPath}\nThe page auto-reloads every 2 seconds. Call design_preview again to update the content live.`,
107
+ },
108
+ ],
109
+ details: { path: previewPath },
110
+ };
111
+ } catch (error) {
112
+ const msg =
113
+ error instanceof Error ? error.message : String(error);
114
+ return {
115
+ content: [
116
+ {
117
+ type: "text",
118
+ text: `Preview failed: ${msg}`,
119
+ },
120
+ ],
121
+ details: {},
122
+ };
123
+ }
124
+ },
125
+ });
126
+
127
+ // ── Tool 2: screenshot_url ──
128
+ // Take a screenshot of a URL for visual QA
129
+ elyra.registerTool({
130
+ name: "screenshot_url",
131
+ label: "Screenshot URL",
132
+ description:
133
+ "Take a screenshot of a web page at a given URL. " +
134
+ "Returns the screenshot as an image that can be analyzed visually. " +
135
+ "Use this for visual QA: after generating UI code, take a screenshot " +
136
+ "and evaluate if the design looks correct. " +
137
+ "Works with localhost URLs for testing local development servers. " +
138
+ "Requires the 'screenshot' CLI tool (macOS built-in) or Puppeteer.",
139
+ parameters: Type.Object({
140
+ url: Type.String({
141
+ description:
142
+ "URL to screenshot (e.g., http://localhost:8000, or file path from design_preview)",
143
+ }),
144
+ width: Type.Optional(
145
+ Type.Number({
146
+ description: "Viewport width in pixels (default: 1280)",
147
+ }),
148
+ ),
149
+ height: Type.Optional(
150
+ Type.Number({
151
+ description: "Viewport height in pixels (default: 800)",
152
+ }),
153
+ ),
154
+ mobile: Type.Optional(
155
+ Type.Boolean({
156
+ description:
157
+ "Use mobile viewport (375x812) (default: false)",
158
+ }),
159
+ ),
160
+ }),
161
+ execute: async (_toolCallId, params) => {
162
+ try {
163
+ if (!existsSync(PREVIEW_DIR)) {
164
+ mkdirSync(PREVIEW_DIR, { recursive: true });
165
+ }
166
+
167
+ const screenshotPath = join(
168
+ PREVIEW_DIR,
169
+ `screenshot-${Date.now()}.png`,
170
+ );
171
+ const width = params.mobile
172
+ ? 375
173
+ : (params.width ?? 1280);
174
+ const height = params.mobile
175
+ ? 812
176
+ : (params.height ?? 800);
177
+
178
+ // Try Puppeteer first (if installed globally or in project)
179
+ let captured = false;
180
+
181
+ try {
182
+ // Write a small Node script that uses puppeteer
183
+ const scriptPath = join(PREVIEW_DIR, "capture.mjs");
184
+ writeFileSync(
185
+ scriptPath,
186
+ `
187
+ import puppeteer from 'puppeteer';
188
+ const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });
189
+ const page = await browser.newPage();
190
+ await page.setViewport({ width: ${width}, height: ${height} });
191
+ await page.goto('${params.url}', { waitUntil: 'networkidle2', timeout: 15000 });
192
+ await page.screenshot({ path: '${screenshotPath}', fullPage: false });
193
+ await browser.close();
194
+ `,
195
+ "utf-8",
196
+ );
197
+
198
+ execSync(`node "${scriptPath}"`, {
199
+ timeout: 30000,
200
+ stdio: "pipe",
201
+ });
202
+ captured = existsSync(screenshotPath);
203
+ } catch {
204
+ // Puppeteer not available
205
+ }
206
+
207
+ // Fallback: macOS screencapture (if the URL is a local file)
208
+ if (
209
+ !captured &&
210
+ process.platform === "darwin" &&
211
+ params.url.startsWith("file://")
212
+ ) {
213
+ try {
214
+ execSync(
215
+ `screencapture -x -C "${screenshotPath}"`,
216
+ { timeout: 10000 },
217
+ );
218
+ captured = existsSync(screenshotPath);
219
+ } catch {
220
+ // screencapture failed
221
+ }
222
+ }
223
+
224
+ if (!captured) {
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: `Could not capture screenshot. Install Puppeteer for automated screenshots:\n\nnpm install -g puppeteer\n\nOr take a manual screenshot and paste it into the chat.`,
230
+ },
231
+ ],
232
+ details: {},
233
+ };
234
+ }
235
+
236
+ // Read the screenshot and return as image content
237
+ const imageData = readFileSync(screenshotPath);
238
+ const base64 = imageData.toString("base64");
239
+
240
+ return {
241
+ content: [
242
+ {
243
+ type: "image",
244
+ data: base64,
245
+ mimeType: "image/png",
246
+ },
247
+ {
248
+ type: "text",
249
+ text: `Screenshot captured (${width}x${height}): ${screenshotPath}\nAnalyze the screenshot for visual issues: alignment, spacing, color contrast, responsive layout, text readability.`,
250
+ },
251
+ ],
252
+ details: { path: screenshotPath, width, height },
253
+ };
254
+ } catch (error) {
255
+ const msg =
256
+ error instanceof Error ? error.message : String(error);
257
+ return {
258
+ content: [
259
+ {
260
+ type: "text",
261
+ text: `Screenshot failed: ${msg}`,
262
+ },
263
+ ],
264
+ details: {},
265
+ };
266
+ }
267
+ },
268
+ });
269
+
270
+ // ── Tool 3: design_system_check ──
271
+ // Analyze Tailwind classes for consistency
272
+ elyra.registerTool({
273
+ name: "design_system_check",
274
+ label: "Design System Check",
275
+ description:
276
+ "Analyze a file or HTML snippet for Tailwind CSS consistency issues. " +
277
+ "Checks for: mixed spacing scales, inconsistent colors, missing responsive prefixes, " +
278
+ "accessibility issues (contrast, focus states), and unused or conflicting classes. " +
279
+ "Use this to ensure UI code follows a consistent design system.",
280
+ parameters: Type.Object({
281
+ file_path: Type.Optional(
282
+ Type.String({
283
+ description:
284
+ "Path to a file to analyze (Vue, Blade, TSX, HTML)",
285
+ }),
286
+ ),
287
+ html: Type.Optional(
288
+ Type.String({
289
+ description:
290
+ "HTML snippet to analyze (alternative to file_path)",
291
+ }),
292
+ ),
293
+ }),
294
+ execute: async (_toolCallId, params) => {
295
+ try {
296
+ let content: string;
297
+ if (params.file_path) {
298
+ content = readFileSync(params.file_path, "utf-8");
299
+ } else if (params.html) {
300
+ content = params.html;
301
+ } else {
302
+ return {
303
+ content: [
304
+ {
305
+ type: "text",
306
+ text: "Provide either file_path or html to analyze.",
307
+ },
308
+ ],
309
+ details: {},
310
+ };
311
+ }
312
+
313
+ const issues: string[] = [];
314
+
315
+ // Check for mixed spacing scales
316
+ const spacingValues = new Set<string>();
317
+ const spacingMatches =
318
+ content.match(
319
+ /(?:p|m|gap|space)-(?:x-|y-)?(\d+)/g,
320
+ ) ?? [];
321
+ for (const match of spacingMatches) {
322
+ const num = match.match(/(\d+)$/)?.[1];
323
+ if (num) spacingValues.add(num);
324
+ }
325
+ if (spacingValues.size > 6) {
326
+ issues.push(
327
+ `[Spacing] ${spacingValues.size} different spacing values used (${[...spacingValues].sort((a, b) => Number(a) - Number(b)).join(", ")}). Consider consolidating to a consistent scale.`,
328
+ );
329
+ }
330
+
331
+ // Check for missing responsive prefixes on layout classes
332
+ const gridCols =
333
+ content.match(/grid-cols-\d+/g) ?? [];
334
+ const responsiveGridCols =
335
+ content.match(
336
+ /(?:sm|md|lg|xl):grid-cols-\d+/g,
337
+ ) ?? [];
338
+ if (
339
+ gridCols.length > 0 &&
340
+ responsiveGridCols.length === 0
341
+ ) {
342
+ issues.push(
343
+ "[Responsive] Grid columns used without responsive breakpoints. Add sm:/md:/lg: prefixes for mobile support.",
344
+ );
345
+ }
346
+
347
+ // Check for missing focus/hover states on interactive elements
348
+ const hasButtons = /button|btn|click|submit/i.test(
349
+ content,
350
+ );
351
+ const hasFocusStates =
352
+ /focus:|focus-visible:/i.test(content);
353
+ const hasHoverStates = /hover:/i.test(content);
354
+ if (hasButtons && !hasFocusStates) {
355
+ issues.push(
356
+ "[Accessibility] Interactive elements found without focus states. Add focus:ring or focus-visible: classes.",
357
+ );
358
+ }
359
+ if (hasButtons && !hasHoverStates) {
360
+ issues.push(
361
+ "[UX] Interactive elements found without hover states. Add hover: classes for visual feedback.",
362
+ );
363
+ }
364
+
365
+ // Check for hardcoded colors vs theme colors
366
+ const hardcodedColors =
367
+ content.match(
368
+ /(?:bg|text|border)-\[#[0-9a-fA-F]+\]/g,
369
+ ) ?? [];
370
+ if (hardcodedColors.length > 0) {
371
+ issues.push(
372
+ `[Colors] ${hardcodedColors.length} hardcoded color values found. Consider using Tailwind theme colors for consistency.`,
373
+ );
374
+ }
375
+
376
+ // Check for conflicting classes
377
+ const hasHidden =
378
+ /\bhidden\b/.test(content) &&
379
+ /\bblock\b/.test(content);
380
+ if (hasHidden) {
381
+ issues.push(
382
+ "[Conflict] Both 'hidden' and 'block' classes found. These may conflict unless used with responsive prefixes.",
383
+ );
384
+ }
385
+
386
+ // Check for missing dark mode support
387
+ const hasDarkClasses = /dark:/i.test(content);
388
+ const hasBgColor =
389
+ /bg-(?:white|gray|slate|zinc)/i.test(content);
390
+ if (hasBgColor && !hasDarkClasses) {
391
+ issues.push(
392
+ "[Dark Mode] Background colors used without dark: variants. Add dark: prefixes for dark mode support.",
393
+ );
394
+ }
395
+
396
+ // Check for text truncation without overflow handling
397
+ const hasTruncate =
398
+ /truncate|line-clamp/i.test(content);
399
+ const hasOverflow =
400
+ /overflow-hidden|overflow-auto/i.test(content);
401
+ if (hasTruncate && !hasOverflow) {
402
+ issues.push(
403
+ "[Layout] Text truncation used without overflow handling on parent container.",
404
+ );
405
+ }
406
+
407
+ const report: string[] = [
408
+ `# Design System Check${params.file_path ? `: ${params.file_path}` : ""}`,
409
+ "",
410
+ ];
411
+
412
+ if (issues.length === 0) {
413
+ report.push(
414
+ "No design consistency issues found.",
415
+ );
416
+ } else {
417
+ report.push(
418
+ `Found ${issues.length} issue${issues.length > 1 ? "s" : ""}:`,
419
+ "",
420
+ );
421
+ for (const issue of issues) {
422
+ report.push(`- ${issue}`);
423
+ }
424
+ }
425
+
426
+ return {
427
+ content: [
428
+ { type: "text", text: report.join("\n") },
429
+ ],
430
+ details: { issueCount: issues.length },
431
+ };
432
+ } catch (error) {
433
+ const msg =
434
+ error instanceof Error ? error.message : String(error);
435
+ return {
436
+ content: [
437
+ {
438
+ type: "text",
439
+ text: `Design check failed: ${msg}`,
440
+ },
441
+ ],
442
+ details: {},
443
+ };
444
+ }
445
+ },
446
+ });
447
+
448
+ // ── Command: /design ──
449
+ elyra.registerCommand("design", {
450
+ description:
451
+ "Design tools: live preview, screenshots, design system check",
452
+ handler: async (_args: string, ctx) => {
453
+ const options = [
454
+ "Preview -- render HTML/Tailwind in the browser",
455
+ "Screenshot -- capture a web page for visual QA",
456
+ "Design Check -- analyze Tailwind classes for consistency",
457
+ ];
458
+
459
+ const selected = await ctx.ui.select(
460
+ "Design Tools",
461
+ options,
462
+ );
463
+ if (!selected) return;
464
+
465
+ if (selected.startsWith("Preview")) {
466
+ elyra.sendUserMessage(
467
+ "I want to preview a UI component. Describe what you want to see or give me the HTML/Tailwind code.",
468
+ );
469
+ } else if (selected.startsWith("Screenshot")) {
470
+ elyra.sendUserMessage(
471
+ "I want to take a screenshot of a page for visual QA. What URL should I capture?",
472
+ );
473
+ } else if (selected.startsWith("Design Check")) {
474
+ elyra.sendUserMessage(
475
+ "I want to check a file for Tailwind design consistency. Which file should I analyze?",
476
+ );
477
+ }
478
+ },
479
+ });
480
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@elyracode/design-tools",
3
+ "version": "0.4.7",
4
+ "description": "Elyra extension for UI design -- live browser preview, screenshot capture, and visual QA",
5
+ "type": "module",
6
+ "keywords": [
7
+ "elyra-package",
8
+ "design",
9
+ "preview",
10
+ "screenshot",
11
+ "visual-qa",
12
+ "tailwind",
13
+ "ui"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Knut W. Horne",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/kwhorne/elyra.git",
20
+ "directory": "packages/design-tools"
21
+ },
22
+ "elyra": {
23
+ "extensions": [
24
+ "./extensions/index.ts"
25
+ ]
26
+ },
27
+ "peerDependencies": {
28
+ "@elyracode/coding-agent": "*",
29
+ "typebox": "*"
30
+ },
31
+ "scripts": {
32
+ "clean": "echo 'nothing to clean'",
33
+ "build": "echo 'nothing to build'",
34
+ "check": "echo 'nothing to check'"
35
+ }
36
+ }