Analyzes Figma designs and generates detailed specifications using AI
Size
25.9 KB
Version
1.0.1
Created
Oct 30, 2025
Updated
15 days ago
1// ==UserScript==
2// @name Figma Design Specification Generator
3// @description Analyzes Figma designs and generates detailed specifications using AI
4// @version 1.0.1
5// @match https://*.figma.com/*
6// @icon https://static.figma.com/app/icon/1/favicon.svg
7// @grant GM.xmlhttpRequest
8// @grant GM.getValue
9// @grant GM.setValue
10// ==/UserScript==
11(function() {
12 'use strict';
13
14 console.log('Figma Design Specification Generator loaded');
15
16 // Debounce function to prevent excessive calls
17 function debounce(func, wait) {
18 let timeout;
19 return function executedFunction(...args) {
20 const later = () => {
21 clearTimeout(timeout);
22 func(...args);
23 };
24 clearTimeout(timeout);
25 timeout = setTimeout(later, wait);
26 };
27 }
28
29 // Add custom styles for the specification panel
30 function addStyles() {
31 const styles = `
32 .spec-gen-button {
33 position: fixed;
34 top: 16px;
35 right: 80px;
36 z-index: 10000;
37 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
38 color: white;
39 border: none;
40 padding: 10px 20px;
41 border-radius: 8px;
42 font-size: 14px;
43 font-weight: 600;
44 cursor: pointer;
45 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
46 transition: all 0.3s ease;
47 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
48 }
49
50 .spec-gen-button:hover {
51 transform: translateY(-2px);
52 box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5);
53 }
54
55 .spec-gen-button:active {
56 transform: translateY(0);
57 }
58
59 .spec-gen-button:disabled {
60 opacity: 0.6;
61 cursor: not-allowed;
62 transform: none;
63 }
64
65 .spec-panel {
66 position: fixed;
67 top: 0;
68 right: 0;
69 width: 500px;
70 height: 100vh;
71 background: #ffffff;
72 box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
73 z-index: 9999;
74 display: flex;
75 flex-direction: column;
76 transform: translateX(100%);
77 transition: transform 0.3s ease;
78 }
79
80 .spec-panel.open {
81 transform: translateX(0);
82 }
83
84 .spec-panel-header {
85 padding: 20px 24px;
86 border-bottom: 1px solid #e5e5e5;
87 display: flex;
88 justify-content: space-between;
89 align-items: center;
90 background: #f8f9fa;
91 }
92
93 .spec-panel-title {
94 font-size: 18px;
95 font-weight: 700;
96 color: #1a1a1a;
97 margin: 0;
98 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
99 }
100
101 .spec-panel-close {
102 background: none;
103 border: none;
104 font-size: 24px;
105 cursor: pointer;
106 color: #666;
107 padding: 0;
108 width: 32px;
109 height: 32px;
110 display: flex;
111 align-items: center;
112 justify-content: center;
113 border-radius: 4px;
114 transition: background 0.2s;
115 }
116
117 .spec-panel-close:hover {
118 background: #e5e5e5;
119 }
120
121 .spec-panel-content {
122 flex: 1;
123 overflow-y: auto;
124 padding: 24px;
125 }
126
127 .spec-loading {
128 display: flex;
129 flex-direction: column;
130 align-items: center;
131 justify-content: center;
132 height: 100%;
133 color: #666;
134 }
135
136 .spec-spinner {
137 width: 48px;
138 height: 48px;
139 border: 4px solid #f3f3f3;
140 border-top: 4px solid #667eea;
141 border-radius: 50%;
142 animation: spin 1s linear infinite;
143 margin-bottom: 16px;
144 }
145
146 @keyframes spin {
147 0% { transform: rotate(0deg); }
148 100% { transform: rotate(360deg); }
149 }
150
151 .spec-section {
152 margin-bottom: 28px;
153 }
154
155 .spec-section-title {
156 font-size: 16px;
157 font-weight: 700;
158 color: #1a1a1a;
159 margin: 0 0 12px 0;
160 padding-bottom: 8px;
161 border-bottom: 2px solid #667eea;
162 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
163 }
164
165 .spec-section-content {
166 font-size: 14px;
167 line-height: 1.6;
168 color: #333;
169 white-space: pre-wrap;
170 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
171 }
172
173 .spec-list {
174 list-style: none;
175 padding: 0;
176 margin: 0;
177 }
178
179 .spec-list-item {
180 padding: 8px 0;
181 border-bottom: 1px solid #f0f0f0;
182 font-size: 14px;
183 color: #333;
184 }
185
186 .spec-list-item:last-child {
187 border-bottom: none;
188 }
189
190 .spec-error {
191 background: #fee;
192 border: 1px solid #fcc;
193 border-radius: 8px;
194 padding: 16px;
195 color: #c33;
196 font-size: 14px;
197 }
198
199 .spec-copy-button {
200 background: #667eea;
201 color: white;
202 border: none;
203 padding: 10px 20px;
204 border-radius: 6px;
205 font-size: 14px;
206 font-weight: 600;
207 cursor: pointer;
208 width: 100%;
209 margin-top: 16px;
210 transition: background 0.2s;
211 }
212
213 .spec-copy-button:hover {
214 background: #5568d3;
215 }
216
217 .spec-copy-button:active {
218 background: #4a5bc4;
219 }
220 `;
221
222 const styleElement = document.createElement('style');
223 styleElement.textContent = styles;
224 document.head.appendChild(styleElement);
225 }
226
227 // Capture screenshot of the current design
228 async function captureDesignScreenshot() {
229 try {
230 console.log('Capturing design screenshot...');
231
232 // Find the canvas element
233 const canvas = document.querySelector('canvas');
234 if (!canvas) {
235 throw new Error('Canvas not found');
236 }
237
238 // Convert canvas to data URL
239 const dataUrl = canvas.toDataURL('image/png');
240 console.log('Screenshot captured successfully');
241 return dataUrl;
242 } catch (error) {
243 console.error('Error capturing screenshot:', error);
244 throw error;
245 }
246 }
247
248 // Extract design information from the page
249 function extractDesignInfo() {
250 const info = {
251 url: window.location.href,
252 title: document.title,
253 selectedNode: null
254 };
255
256 // Try to get selected node information from URL
257 const urlParams = new URLSearchParams(window.location.search);
258 const nodeId = urlParams.get('node-id');
259 if (nodeId) {
260 info.selectedNode = nodeId;
261 }
262
263 // Try to extract any visible text content from the design
264 const textElements = [];
265 document.querySelectorAll('[data-testid*="text"], [class*="text"]').forEach(el => {
266 const text = el.textContent?.trim();
267 if (text && text.length > 0 && text.length < 200) {
268 textElements.push(text);
269 }
270 });
271 info.visibleText = textElements.slice(0, 10); // Limit to first 10 text elements
272
273 return info;
274 }
275
276 // Generate specification using AI
277 async function generateSpecification(screenshot, designInfo) {
278 console.log('Generating specification with AI...');
279
280 const prompt = `You are a professional design specification writer. Analyze this Figma design and create a comprehensive, detailed specification document.
281
282Design Context:
283- File: ${designInfo.title}
284- URL: ${designInfo.url}
285${designInfo.selectedNode ? `- Selected Node: ${designInfo.selectedNode}` : ''}
286${designInfo.visibleText?.length > 0 ? `- Visible Text Elements: ${designInfo.visibleText.join(', ')}` : ''}
287
288Based on the design screenshot, create a detailed specification that includes:
289
2901. **Overview**: Brief description of what this design represents (screen, component, feature, etc.)
291
2922. **Layout & Structure**: Describe the overall layout, grid system, spacing, and how elements are organized
293
2943. **Components**: List all UI components visible (buttons, inputs, cards, navigation, etc.) with their properties
295
2964. **Typography**: Font families, sizes, weights, line heights, and text styles used
297
2985. **Colors**: Color palette including primary, secondary, background, text colors with hex codes if visible
299
3006. **Spacing & Sizing**: Margins, paddings, element dimensions, and spacing patterns
301
3027. **Interactive Elements**: Buttons, links, form fields and their states (hover, active, disabled, etc.)
303
3048. **Responsive Behavior**: How the design should adapt to different screen sizes (if applicable)
305
3069. **Accessibility Considerations**: Color contrast, text sizes, interactive element sizes, ARIA requirements
307
30810. **Implementation Notes**: Any special considerations for developers implementing this design
309
310Please be specific, detailed, and use professional terminology. Format the response as a structured specification document.`;
311
312 try {
313 const response = await RM.aiCall(prompt, {
314 type: "json_schema",
315 json_schema: {
316 name: "design_specification",
317 schema: {
318 type: "object",
319 properties: {
320 overview: { type: "string" },
321 layout: { type: "string" },
322 components: {
323 type: "array",
324 items: {
325 type: "object",
326 properties: {
327 name: { type: "string" },
328 description: { type: "string" },
329 properties: { type: "string" }
330 },
331 required: ["name", "description"]
332 }
333 },
334 typography: {
335 type: "object",
336 properties: {
337 fonts: { type: "array", items: { type: "string" } },
338 styles: { type: "array", items: { type: "string" } }
339 },
340 required: ["fonts", "styles"]
341 },
342 colors: {
343 type: "object",
344 properties: {
345 primary: { type: "array", items: { type: "string" } },
346 secondary: { type: "array", items: { type: "string" } },
347 neutral: { type: "array", items: { type: "string" } }
348 }
349 },
350 spacing: { type: "string" },
351 interactiveElements: { type: "array", items: { type: "string" } },
352 responsive: { type: "string" },
353 accessibility: { type: "array", items: { type: "string" } },
354 implementationNotes: { type: "array", items: { type: "string" } }
355 },
356 required: ["overview", "layout", "components"]
357 }
358 }
359 });
360
361 console.log('Specification generated successfully');
362 return response;
363 } catch (error) {
364 console.error('Error generating specification:', error);
365 throw error;
366 }
367 }
368
369 // Format specification for display
370 function formatSpecification(spec) {
371 let html = '';
372
373 // Overview
374 if (spec.overview) {
375 html += `
376 <div class="spec-section">
377 <h3 class="spec-section-title">📋 Overview</h3>
378 <div class="spec-section-content">${spec.overview}</div>
379 </div>
380 `;
381 }
382
383 // Layout & Structure
384 if (spec.layout) {
385 html += `
386 <div class="spec-section">
387 <h3 class="spec-section-title">📐 Layout & Structure</h3>
388 <div class="spec-section-content">${spec.layout}</div>
389 </div>
390 `;
391 }
392
393 // Components
394 if (spec.components && spec.components.length > 0) {
395 html += `
396 <div class="spec-section">
397 <h3 class="spec-section-title">🧩 Components</h3>
398 <ul class="spec-list">
399 `;
400 spec.components.forEach(component => {
401 html += `
402 <li class="spec-list-item">
403 <strong>${component.name}</strong><br>
404 ${component.description}
405 ${component.properties ? `<br><em>${component.properties}</em>` : ''}
406 </li>
407 `;
408 });
409 html += `</ul></div>`;
410 }
411
412 // Typography
413 if (spec.typography) {
414 html += `
415 <div class="spec-section">
416 <h3 class="spec-section-title">✍️ Typography</h3>
417 `;
418 if (spec.typography.fonts && spec.typography.fonts.length > 0) {
419 html += `<div class="spec-section-content"><strong>Fonts:</strong><br>${spec.typography.fonts.join('<br>')}</div>`;
420 }
421 if (spec.typography.styles && spec.typography.styles.length > 0) {
422 html += `<div class="spec-section-content" style="margin-top: 12px;"><strong>Styles:</strong><br>${spec.typography.styles.join('<br>')}</div>`;
423 }
424 html += `</div>`;
425 }
426
427 // Colors
428 if (spec.colors) {
429 html += `
430 <div class="spec-section">
431 <h3 class="spec-section-title">🎨 Colors</h3>
432 `;
433 if (spec.colors.primary && spec.colors.primary.length > 0) {
434 html += `<div class="spec-section-content"><strong>Primary:</strong><br>${spec.colors.primary.join('<br>')}</div>`;
435 }
436 if (spec.colors.secondary && spec.colors.secondary.length > 0) {
437 html += `<div class="spec-section-content" style="margin-top: 8px;"><strong>Secondary:</strong><br>${spec.colors.secondary.join('<br>')}</div>`;
438 }
439 if (spec.colors.neutral && spec.colors.neutral.length > 0) {
440 html += `<div class="spec-section-content" style="margin-top: 8px;"><strong>Neutral:</strong><br>${spec.colors.neutral.join('<br>')}</div>`;
441 }
442 html += `</div>`;
443 }
444
445 // Spacing
446 if (spec.spacing) {
447 html += `
448 <div class="spec-section">
449 <h3 class="spec-section-title">📏 Spacing & Sizing</h3>
450 <div class="spec-section-content">${spec.spacing}</div>
451 </div>
452 `;
453 }
454
455 // Interactive Elements
456 if (spec.interactiveElements && spec.interactiveElements.length > 0) {
457 html += `
458 <div class="spec-section">
459 <h3 class="spec-section-title">🖱️ Interactive Elements</h3>
460 <ul class="spec-list">
461 ${spec.interactiveElements.map(item => `<li class="spec-list-item">${item}</li>`).join('')}
462 </ul>
463 </div>
464 `;
465 }
466
467 // Responsive Behavior
468 if (spec.responsive) {
469 html += `
470 <div class="spec-section">
471 <h3 class="spec-section-title">📱 Responsive Behavior</h3>
472 <div class="spec-section-content">${spec.responsive}</div>
473 </div>
474 `;
475 }
476
477 // Accessibility
478 if (spec.accessibility && spec.accessibility.length > 0) {
479 html += `
480 <div class="spec-section">
481 <h3 class="spec-section-title">♿ Accessibility</h3>
482 <ul class="spec-list">
483 ${spec.accessibility.map(item => `<li class="spec-list-item">${item}</li>`).join('')}
484 </ul>
485 </div>
486 `;
487 }
488
489 // Implementation Notes
490 if (spec.implementationNotes && spec.implementationNotes.length > 0) {
491 html += `
492 <div class="spec-section">
493 <h3 class="spec-section-title">💻 Implementation Notes</h3>
494 <ul class="spec-list">
495 ${spec.implementationNotes.map(item => `<li class="spec-list-item">${item}</li>`).join('')}
496 </ul>
497 </div>
498 `;
499 }
500
501 return html;
502 }
503
504 // Convert specification to plain text for copying
505 function specificationToText(spec) {
506 let text = 'DESIGN SPECIFICATION\n';
507 text += '='.repeat(50) + '\n\n';
508
509 if (spec.overview) {
510 text += 'OVERVIEW\n' + '-'.repeat(50) + '\n' + spec.overview + '\n\n';
511 }
512
513 if (spec.layout) {
514 text += 'LAYOUT & STRUCTURE\n' + '-'.repeat(50) + '\n' + spec.layout + '\n\n';
515 }
516
517 if (spec.components && spec.components.length > 0) {
518 text += 'COMPONENTS\n' + '-'.repeat(50) + '\n';
519 spec.components.forEach((component, index) => {
520 text += `${index + 1}. ${component.name}\n ${component.description}\n`;
521 if (component.properties) {
522 text += ` Properties: ${component.properties}\n`;
523 }
524 text += '\n';
525 });
526 }
527
528 if (spec.typography) {
529 text += 'TYPOGRAPHY\n' + '-'.repeat(50) + '\n';
530 if (spec.typography.fonts) {
531 text += 'Fonts:\n' + spec.typography.fonts.map(f => ' - ' + f).join('\n') + '\n\n';
532 }
533 if (spec.typography.styles) {
534 text += 'Styles:\n' + spec.typography.styles.map(s => ' - ' + s).join('\n') + '\n\n';
535 }
536 }
537
538 if (spec.colors) {
539 text += 'COLORS\n' + '-'.repeat(50) + '\n';
540 if (spec.colors.primary) {
541 text += 'Primary:\n' + spec.colors.primary.map(c => ' - ' + c).join('\n') + '\n';
542 }
543 if (spec.colors.secondary) {
544 text += 'Secondary:\n' + spec.colors.secondary.map(c => ' - ' + c).join('\n') + '\n';
545 }
546 if (spec.colors.neutral) {
547 text += 'Neutral:\n' + spec.colors.neutral.map(c => ' - ' + c).join('\n') + '\n';
548 }
549 text += '\n';
550 }
551
552 if (spec.spacing) {
553 text += 'SPACING & SIZING\n' + '-'.repeat(50) + '\n' + spec.spacing + '\n\n';
554 }
555
556 if (spec.interactiveElements && spec.interactiveElements.length > 0) {
557 text += 'INTERACTIVE ELEMENTS\n' + '-'.repeat(50) + '\n';
558 text += spec.interactiveElements.map(item => ' - ' + item).join('\n') + '\n\n';
559 }
560
561 if (spec.responsive) {
562 text += 'RESPONSIVE BEHAVIOR\n' + '-'.repeat(50) + '\n' + spec.responsive + '\n\n';
563 }
564
565 if (spec.accessibility && spec.accessibility.length > 0) {
566 text += 'ACCESSIBILITY\n' + '-'.repeat(50) + '\n';
567 text += spec.accessibility.map(item => ' - ' + item).join('\n') + '\n\n';
568 }
569
570 if (spec.implementationNotes && spec.implementationNotes.length > 0) {
571 text += 'IMPLEMENTATION NOTES\n' + '-'.repeat(50) + '\n';
572 text += spec.implementationNotes.map(item => ' - ' + item).join('\n') + '\n\n';
573 }
574
575 return text;
576 }
577
578 // Create the specification panel
579 function createSpecPanel() {
580 const panel = document.createElement('div');
581 panel.className = 'spec-panel';
582 panel.innerHTML = `
583 <div class="spec-panel-header">
584 <h2 class="spec-panel-title">Design Specification</h2>
585 <button class="spec-panel-close" aria-label="Close panel">×</button>
586 </div>
587 <div class="spec-panel-content">
588 <div class="spec-loading">
589 <div class="spec-spinner"></div>
590 <p>Analyzing design and generating specification...</p>
591 </div>
592 </div>
593 `;
594
595 document.body.appendChild(panel);
596
597 // Close button handler
598 const closeButton = panel.querySelector('.spec-panel-close');
599 closeButton.addEventListener('click', () => {
600 panel.classList.remove('open');
601 });
602
603 return panel;
604 }
605
606 // Update panel with specification content
607 function updatePanelContent(panel, spec) {
608 const content = panel.querySelector('.spec-panel-content');
609 const formattedSpec = formatSpecification(spec);
610
611 content.innerHTML = `
612 ${formattedSpec}
613 <button class="spec-copy-button">📋 Copy Specification to Clipboard</button>
614 `;
615
616 // Add copy button handler
617 const copyButton = content.querySelector('.spec-copy-button');
618 copyButton.addEventListener('click', async () => {
619 const textSpec = specificationToText(spec);
620 try {
621 await GM.setClipboard(textSpec);
622 copyButton.textContent = '✅ Copied to Clipboard!';
623 setTimeout(() => {
624 copyButton.textContent = '📋 Copy Specification to Clipboard';
625 }, 2000);
626 } catch (error) {
627 console.error('Error copying to clipboard:', error);
628 copyButton.textContent = '❌ Copy Failed';
629 setTimeout(() => {
630 copyButton.textContent = '📋 Copy Specification to Clipboard';
631 }, 2000);
632 }
633 });
634 }
635
636 // Show error in panel
637 function showPanelError(panel, error) {
638 const content = panel.querySelector('.spec-panel-content');
639 content.innerHTML = `
640 <div class="spec-error">
641 <strong>Error generating specification:</strong><br>
642 ${error.message || 'An unexpected error occurred. Please try again.'}
643 </div>
644 `;
645 }
646
647 // Main function to generate and display specification
648 async function handleGenerateSpec(button) {
649 try {
650 button.disabled = true;
651 button.textContent = '⏳ Generating...';
652
653 // Create and show panel
654 const panel = createSpecPanel();
655 setTimeout(() => panel.classList.add('open'), 10);
656
657 // Capture screenshot
658 const screenshot = await captureDesignScreenshot();
659
660 // Extract design info
661 const designInfo = extractDesignInfo();
662
663 // Generate specification
664 const spec = await generateSpecification(screenshot, designInfo);
665
666 // Update panel with results
667 updatePanelContent(panel, spec);
668
669 // Save to storage for later access
670 await GM.setValue('lastSpecification', JSON.stringify(spec));
671 await GM.setValue('lastSpecificationTime', Date.now());
672
673 } catch (error) {
674 console.error('Error in handleGenerateSpec:', error);
675 const panel = document.querySelector('.spec-panel');
676 if (panel) {
677 showPanelError(panel, error);
678 } else {
679 alert('Error generating specification: ' + error.message);
680 }
681 } finally {
682 button.disabled = false;
683 button.textContent = '✨ Generate Spec';
684 }
685 }
686
687 // Create the generate button
688 function createGenerateButton() {
689 const button = document.createElement('button');
690 button.className = 'spec-gen-button';
691 button.textContent = '✨ Generate Spec';
692 button.title = 'Generate design specification using AI';
693
694 button.addEventListener('click', () => handleGenerateSpec(button));
695
696 document.body.appendChild(button);
697 console.log('Generate Spec button added to page');
698 }
699
700 // Initialize the extension
701 function init() {
702 console.log('Initializing Figma Design Specification Generator...');
703
704 // Add styles
705 addStyles();
706
707 // Wait for the page to be fully loaded
708 if (document.readyState === 'loading') {
709 document.addEventListener('DOMContentLoaded', createGenerateButton);
710 } else {
711 createGenerateButton();
712 }
713
714 // Re-add button if it gets removed (e.g., during navigation)
715 const observer = new MutationObserver(debounce(() => {
716 if (!document.querySelector('.spec-gen-button')) {
717 console.log('Button removed, re-adding...');
718 createGenerateButton();
719 }
720 }, 1000));
721
722 observer.observe(document.body, {
723 childList: true,
724 subtree: true
725 });
726 }
727
728 // Start the extension
729 init();
730
731})();