🎯 Рекомендуемые коллекции
Балансированные коллекции примеров кода из различных категорий, которые вы можете исследовать
Примеры Figma Plugins
Примеры разработки плагинов Figma с созданием UI, автоматизацией дизайна и инструментами улучшения рабочих процессов
💻 Плагин Figma Hello World typescript
🟢 simple
⭐⭐
Базовая настройка плагина Figma с созданием UI, взаимодействием API и основами передачи сообщений
⏱️ 20 min
🏷️ figma, plugin, typescript, design tools
Prerequisites:
TypeScript basics, Figma API knowledge, HTML/CSS
// Figma Plugin Hello World
// Basic plugin structure with UI and API interaction
// Manifest file (manifest.json):
/*
{
"name": "Hello World Plugin",
"id": "YOUR_UNIQUE_PLUGIN_ID",
"api": "1.0.0",
"main": "dist/code.js",
"ui": "dist/ui.html",
"editorType": ["figma"]
}
*/
// code.ts - Main plugin logic
import { showUI } from '@create-figma-plugin/utilities'
export default function () {
figma.currentPage.selection = figma.currentPage.selection
// Create initial UI state
const initialState = {
count: 0,
message: 'Hello from Figma Plugin!',
selectedNodes: figma.currentPage.selection.length,
user: figma.currentUser?.name || 'Anonymous'
}
showUI({ height: 400, width: 320 }, initialState)
}
// Message handling from UI
figma.ui.onmessage = async (msg: any) => {
switch (msg.type) {
case 'create-shape':
await createShape(msg.shapeType, msg.properties)
break
case 'get-selection':
const selection = figma.currentPage.selection.map(node => ({
id: node.id,
name: node.name,
type: node.type
}))
figma.ui.postMessage({
type: 'selection-updated',
selection
})
break
case 'update-selection':
if (msg.nodeIds && msg.nodeIds.length > 0) {
const nodes = []
for (const id of msg.nodeIds) {
const node = figma.getNodeById(id)
if (node) nodes.push(node)
}
figma.currentPage.selection = nodes
figma.viewport.scrollAndZoomIntoView(nodes)
}
break
case 'notify':
figma.notify(msg.message)
break
case 'close-plugin':
figma.closePlugin()
break
case 'export-selection':
await exportSelection(msg.format)
break
case 'apply-style':
await applyStyleToSelection(msg.style)
break
default:
console.log('Unknown message type:', msg.type)
}
}
// Shape creation functions
async function createShape(shapeType: string, properties: any) {
let node: SceneNode
switch (shapeType) {
case 'rectangle':
node = figma.createRectangle()
node.resize(properties.width || 100, properties.height || 100)
break
case 'circle':
node = figma.createEllipse()
node.resize(properties.width || 100, properties.height || 100)
break
case 'star':
node = figma.createStar()
;(node as StarNode).pointCount = properties.points || 5
break
case 'polygon':
node = figma.createPolygon()
;(node as PolygonNode).pointCount = properties.sides || 6
break
case 'text':
node = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
;(node as TextNode).characters = properties.text || 'Hello World'
;(node as TextNode).fontSize = properties.fontSize || 24
break
default:
node = figma.createRectangle()
node.resize(100, 100)
}
// Apply common properties
if (properties.x !== undefined && properties.y !== undefined) {
node.x = properties.x
node.y = properties.y
} else {
// Center in viewport
const viewport = figma.viewport.center
node.x = viewport.x - (node.width / 2)
node.y = viewport.y - (node.height / 2)
}
if (properties.fill) {
if ('fills' in node) {
node.fills = [{
type: 'SOLID',
color: {
r: properties.fill.r || 0.5,
g: properties.fill.g || 0.5,
b: properties.fill.b || 1.0
}
}]
}
}
if (properties.name) {
node.name = properties.name
}
// Add to page
figma.currentPage.appendChild(node)
figma.currentPage.selection = [node]
// Notify UI
figma.ui.postMessage({
type: 'shape-created',
shape: {
id: node.id,
name: node.name,
type: node.type,
width: node.width,
height: node.height
}
})
}
// Export functionality
async function exportSelection(format: string) {
if (figma.currentPage.selection.length === 0) {
figma.notify('Please select something to export')
return
}
try {
switch (format) {
case 'svg':
await exportAsSVG()
break
case 'png':
await exportAsPNG()
break
case 'json':
await exportAsJSON()
break
default:
figma.notify('Unsupported export format')
}
} catch (error) {
figma.notify(`Export failed: ${error.message}`)
}
}
async function exportAsSVG() {
const nodes = figma.currentPage.selection
for (const node of nodes) {
if ('exportAsync' in node) {
const bytes = await node.exportAsync({
format: 'SVG',
contentsOnly: true
})
figma.ui.postMessage({
type: 'export-complete',
data: Array.from(bytes),
filename: `${node.name}.svg`
})
}
}
}
async function exportAsPNG() {
const nodes = figma.currentPage.selection
for (const node of nodes) {
if ('exportAsync' in node) {
const bytes = await node.exportAsync({
format: 'PNG',
constraint: { type: 'SCALE', value: 2 }
})
figma.ui.postMessage({
type: 'export-complete',
data: Array.from(bytes),
filename: `${node.name}.png`
})
}
}
}
async function exportAsJSON() {
const selection = figma.currentPage.selection
const jsonData = JSON.stringify({
nodes: selection.map(node => ({
id: node.id,
name: node.name,
type: node.type,
x: node.x,
y: node.y,
width: node.width,
height: node.height,
visible: node.visible,
locked: node.locked
})),
page: {
name: figma.currentPage.name,
selectionCount: selection.length
}
}, null, 2)
figma.ui.postMessage({
type: 'export-complete',
data: jsonData,
filename: 'selection.json'
})
}
// Style application
async function applyStyleToSelection(style: any) {
const selection = figma.currentPage.selection
for (const node of selection) {
if ('fills' in node && style.fills) {
node.fills = style.fills
}
if ('strokes' in node && style.strokes) {
node.strokes = style.strokes
}
if ('cornerRadius' in node && style.cornerRadius !== undefined) {
node.cornerRadius = style.cornerRadius
}
if ('effects' in node && style.effects) {
node.effects = style.effects
}
}
figma.notify(`Applied style to ${selection.length} nodes`)
}
// ui.html - Plugin interface
/*
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World Plugin</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 16px;
background: #f8f9fa;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
color: #333;
font-size: 18px;
}
.user-info {
color: #666;
font-size: 12px;
margin-top: 4px;
}
.section {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.section-title {
font-weight: 600;
margin-bottom: 12px;
color: #333;
font-size: 14px;
}
.button-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 12px;
}
button {
padding: 8px 12px;
border: none;
border-radius: 6px;
background: #0d99ff;
color: white;
cursor: pointer;
font-size: 12px;
transition: background 0.2s;
}
button:hover {
background: #0d7dd8;
}
button:active {
transform: translateY(1px);
}
button.secondary {
background: #e4e6ea;
color: #333;
}
button.secondary:hover {
background: #d7d9dd;
}
button.danger {
background: #ff4d4d;
}
button.danger:hover {
background: #ff3333;
}
.input-group {
margin-bottom: 12px;
}
label {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #666;
}
input, select, textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
box-sizing: border-box;
}
.status {
background: #e8f4fd;
padding: 12px;
border-radius: 6px;
font-size: 12px;
color: #0d99ff;
text-align: center;
}
.selection-list {
max-height: 120px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
}
.selection-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
border-bottom: 1px solid #eee;
font-size: 12px;
}
.selection-item:last-child {
border-bottom: none;
}
.node-type {
background: #0d99ff;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
}
.color-picker {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.color-option {
width: 32px;
height: 32px;
border-radius: 4px;
cursor: pointer;
border: 2px solid transparent;
}
.color-option:hover {
border-color: #0d99ff;
}
.color-option.selected {
border-color: #0d99ff;
box-shadow: 0 0 0 2px rgba(13, 153, 255, 0.2);
}
</style>
</head>
<body>
<div class="header">
<h2>Hello World Plugin</h2>
<div class="user-info" id="userInfo">Loading...</div>
</div>
<div class="status" id="status">
Ready to create shapes!
</div>
<div class="section">
<div class="section-title">Create Shapes</div>
<div class="button-group">
<button onclick="createShape('rectangle')">Rectangle</button>
<button onclick="createShape('circle')">Circle</button>
<button onclick="createShape('star')">Star</button>
<button onclick="createShape('text')">Text</button>
</div>
<div class="input-group">
<label>Shape Name</label>
<input type="text" id="shapeName" placeholder="Enter shape name">
</div>
<div class="input-group">
<label>Color</label>
<div class="color-picker" id="colorPicker">
<!-- Colors will be added by JavaScript -->
</div>
</div>
</div>
<div class="section">
<div class="section-title">Selection</div>
<div class="button-group">
<button onclick="getSelection()">Refresh</button>
<button class="secondary" onclick="clearSelection()">Clear</button>
</div>
<div class="selection-list" id="selectionList">
<div style="color: #999; text-align: center;">No selection</div>
</div>
</div>
<div class="section">
<div class="section-title">Export</div>
<div class="button-group">
<button onclick="exportSelection('svg')">SVG</button>
<button onclick="exportSelection('png')">PNG</button>
<button onclick="exportSelection('json')">JSON</button>
<button class="secondary" onclick="exportSelection('jpg')">JPG</button>
</div>
</div>
<div class="section">
<div class="section-title">Quick Actions</div>
<div class="button-group">
<button onclick="duplicateSelection()">Duplicate</button>
<button onclick="groupSelection()">Group</button>
<button onclick="alignSelection()">Align</button>
<button class="danger" onclick="deleteSelection()">Delete</button>
</div>
</div>
<script>
let selectedColor = { r: 0.5, g: 0.5, b: 1.0 };
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initializeColorPicker();
setInterval(getSelection, 1000); // Auto-refresh selection
});
// Initialize color picker
function initializeColorPicker() {
const colors = [
{ r: 1.0, g: 0.3, b: 0.3 }, // Red
{ r: 0.3, g: 1.0, b: 0.3 }, // Green
{ r: 0.3, g: 0.3, b: 1.0 }, // Blue
{ r: 1.0, g: 1.0, b: 0.3 }, // Yellow
{ r: 1.0, g: 0.3, b: 1.0 }, // Magenta
{ r: 0.3, g: 1.0, b: 1.0 }, // Cyan
{ r: 1.0, g: 0.5, b: 0.0 }, // Orange
{ r: 0.5, g: 0.5, b: 1.0 }, // Default blue
];
const picker = document.getElementById('colorPicker');
colors.forEach((color, index) => {
const colorDiv = document.createElement('div');
colorDiv.className = 'color-option' + (index === 7 ? ' selected' : '');
colorDiv.style.backgroundColor = `rgb(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)})`;
colorDiv.onclick = () => selectColor(color, colorDiv);
picker.appendChild(colorDiv);
});
}
function selectColor(color, element) {
selectedColor = color;
document.querySelectorAll('.color-option').forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
}
function createShape(type) {
const name = document.getElementById('shapeName').value || `${type}-${Date.now()}`;
parent.postMessage({
pluginMessage: {
type: 'create-shape',
shapeType: type,
properties: {
name: name,
fill: selectedColor,
text: type === 'text' ? 'Hello World' : undefined,
fontSize: type === 'text' ? 24 : undefined,
width: 100,
height: 100
}
}
}, '*');
updateStatus(`Created ${type}: ${name}`);
}
function getSelection() {
parent.postMessage({ pluginMessage: { type: 'get-selection' } }, '*');
}
function clearSelection() {
parent.postMessage({
pluginMessage: {
type: 'update-selection',
nodeIds: []
}
}, '*');
}
function exportSelection(format) {
parent.postMessage({
pluginMessage: {
type: 'export-selection',
format: format
}
}, '*');
}
function duplicateSelection() {
parent.postMessage({
pluginMessage: {
type: 'duplicate-selection'
}
}, '*');
updateStatus('Duplicated selection');
}
function groupSelection() {
parent.postMessage({
pluginMessage: {
type: 'group-selection'
}
}, '*');
updateStatus('Grouped selection');
}
function alignSelection() {
parent.postMessage({
pluginMessage: {
type: 'align-selection'
}
}, '*');
updateStatus('Aligned selection');
}
function deleteSelection() {
parent.postMessage({
pluginMessage: {
type: 'delete-selection'
}
}, '*');
updateStatus('Deleted selection');
}
function updateStatus(message) {
const status = document.getElementById('status');
status.textContent = message;
setTimeout(() => {
status.textContent = 'Ready to create shapes!';
}, 3000);
}
// Handle messages from plugin
window.onmessage = (event) => {
const msg = event.data.pluginMessage;
if (!msg) return;
switch (msg.type) {
case 'initial-state':
document.getElementById('userInfo').textContent = `User: ${msg.user} | Selection: ${msg.selectedNodes}`;
break;
case 'selection-updated':
updateSelectionList(msg.selection);
document.getElementById('userInfo').textContent = `Selection: ${msg.selection.length} nodes`;
break;
case 'shape-created':
updateStatus(`Created: ${msg.shape.name}`);
getSelection(); // Refresh selection
break;
case 'export-complete':
downloadFile(msg.data, msg.filename);
updateStatus(`Exported: ${msg.filename}`);
break;
}
};
function updateSelectionList(selection) {
const list = document.getElementById('selectionList');
if (selection.length === 0) {
list.innerHTML = '<div style="color: #999; text-align: center;">No selection</div>';
return;
}
list.innerHTML = selection.map(node => `
<div class="selection-item">
<span>${node.name}</span>
<span class="node-type">${node.type}</span>
</div>
`).join('');
}
function downloadFile(data, filename) {
if (typeof data === 'string') {
// JSON/text file
const blob = new Blob([data], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
} else {
// Binary data (PNG, SVG)
const blob = new Blob([new Uint8Array(data)], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
}
</script>
</body>
</html>
*/
💻 Менеджер Системы Дизайна typescript
🟡 intermediate
⭐⭐⭐⭐
Полнофункциональный плагин системы дизайна с библиотекой компонентов, управлением стилями и системой токенов
⏱️ 45 min
🏷️ figma, plugin, typescript, design systems
Prerequisites:
Advanced TypeScript, Figma API expertise, Design systems knowledge
// Figma Design System Manager Plugin
// Component library, style management, and design token system
interface DesignToken {
name: string
category: 'color' | 'typography' | 'spacing' | 'effects' | 'border'
value: any
description?: string
}
interface Component {
id: string
name: string
category: string
description: string
properties: Record<string, any>
preview: string
}
interface StyleGuide {
name: string
description: string
tokens: DesignToken[]
components: Component[]
lastUpdated: string
}
// Main plugin code
export default function () {
const designSystem = loadDesignSystem()
showUI({
height: 600,
width: 400
}, {
designSystem,
currentPage: figma.currentPage.name,
selectedCount: figma.currentPage.selection.length
})
}
// Message handling
figma.ui.onmessage = async (msg: any) => {
switch (msg.type) {
case 'save-design-system':
await saveDesignSystem(msg.designSystem)
figma.notify('Design system saved successfully')
break
case 'apply-color-token':
await applyColorToken(msg.tokenName, msg.target)
break
case 'apply-typography-token':
await applyTypographyToken(msg.tokenName, msg.target)
break
case 'apply-spacing-token':
await applySpacingToken(msg.tokenName, msg.target)
break
case 'create-component':
await createDesignComponent(msg.component)
break
case 'generate-style-guide':
await generateStyleGuide(msg.options)
break
case 'extract-tokens':
await extractTokensFromSelection()
break
case 'audit-design':
await auditDesignSystem()
break
case 'sync-with-library':
await syncWithDesignLibrary(msg.libraryId)
break
}
}
// Design System Management
function loadDesignSystem(): StyleGuide {
const stored = figma.root.getPluginData('designSystem')
if (stored) {
try {
return JSON.parse(stored)
} catch (e) {
console.error('Error loading design system:', e)
}
}
// Default design system
return {
name: 'My Design System',
description: 'Default design system',
tokens: getDefaultTokens(),
components: [],
lastUpdated: new Date().toISOString()
}
}
async function saveDesignSystem(designSystem: StyleGuide) {
designSystem.lastUpdated = new Date().toISOString()
figma.root.setPluginData('designSystem', JSON.stringify(designSystem))
}
function getDefaultTokens(): DesignToken[] {
return [
// Color tokens
{
name: 'primary-blue',
category: 'color',
value: { r: 0.13, g: 0.59, b: 1.0 },
description: 'Primary brand color'
},
{
name: 'secondary-gray',
category: 'color',
value: { r: 0.5, g: 0.5, b: 0.5 },
description: 'Secondary text color'
},
{
name: 'success-green',
category: 'color',
value: { r: 0.2, g: 0.8, b: 0.4 },
description: 'Success state color'
},
{
name: 'error-red',
category: 'color',
value: { r: 1.0, g: 0.2, b: 0.2 },
description: 'Error state color'
},
// Typography tokens
{
name: 'heading-large',
category: 'typography',
value: {
fontFamily: 'Inter',
fontWeight: 700,
fontSize: 32,
lineHeight: 40
}
},
{
name: 'body-regular',
category: 'typography',
value: {
fontFamily: 'Inter',
fontWeight: 400,
fontSize: 14,
lineHeight: 20
}
},
// Spacing tokens
{
name: 'spacing-xs',
category: 'spacing',
value: 4,
description: 'Extra small spacing'
},
{
name: 'spacing-sm',
category: 'spacing',
value: 8,
description: 'Small spacing'
},
{
name: 'spacing-md',
category: 'spacing',
value: 16,
description: 'Medium spacing'
},
{
name: 'spacing-lg',
category: 'spacing',
value: 24,
description: 'Large spacing'
},
// Effect tokens
{
name: 'shadow-sm',
category: 'effects',
value: {
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.1 },
offset: { x: 0, y: 2 },
radius: 4
}
},
// Border tokens
{
name: 'border-radius-sm',
category: 'border',
value: 4
},
{
name: 'border-radius-md',
category: 'border',
value: 8
}
]
}
// Token Application Functions
async function applyColorToken(tokenName: string, target: string = 'selection') {
const designSystem = loadDesignSystem()
const token = designSystem.tokens.find(t => t.name === tokenName && t.category === 'color')
if (!token) {
figma.notify('Color token not found')
return
}
const nodes = target === 'selection' ? figma.currentPage.selection : [figma.getNodeById(target)]
for (const node of nodes) {
if ('fills' in node) {
node.fills = [{
type: 'SOLID',
color: token.value
}]
// Add token name as comment for reference
if (!node.getPluginData('colorToken')) {
node.setPluginData('colorToken', tokenName)
}
}
}
figma.notify(`Applied color token: ${tokenName}`)
}
async function applyTypographyToken(tokenName: string, target: string = 'selection') {
const designSystem = loadDesignSystem()
const token = designSystem.tokens.find(t => t.name === tokenName && t.category === 'typography')
if (!token) {
figma.notify('Typography token not found')
return
}
const nodes = target === 'selection' ? figma.currentPage.selection : [figma.getNodeById(target)]
for (const node of nodes) {
if (node.type === 'TEXT') {
const textNode = node as TextNode
// Load font
await figma.loadFontAsync({
family: token.value.fontFamily,
style: getFontStyle(token.value.fontWeight)
})
// Apply typography styles
textNode.fontName = { family: token.value.fontFamily, style: getFontStyle(token.value.fontWeight) }
textNode.fontSize = token.value.fontSize
textNode.lineHeight = { unit: 'PIXELS', value: token.value.lineHeight }
// Store token reference
textNode.setPluginData('typographyToken', tokenName)
}
}
figma.notify(`Applied typography token: ${tokenName}`)
}
async function applySpacingToken(tokenName: string, target: string = 'selection') {
const designSystem = loadDesignSystem()
const token = designSystem.tokens.find(t => t.name === tokenName && t.category === 'spacing')
if (!token) {
figma.notify('Spacing token not found')
return
}
const nodes = target === 'selection' ? figma.currentPage.selection : [figma.getNodeById(target)]
for (const node of nodes) {
if ('layoutMode' in node && node.layoutMode !== 'NONE') {
// Auto layout node
const frameNode = node as FrameNode
if (frameNode.itemSpacing !== token.value) {
frameNode.itemSpacing = token.value
}
} else {
// Regular node - apply padding
if ('paddingTop' in node) {
const containerNode = node as FrameNode
containerNode.paddingTop = token.value
containerNode.paddingBottom = token.value
containerNode.paddingLeft = token.value
containerNode.paddingRight = token.value
}
}
node.setPluginData('spacingToken', tokenName)
}
figma.notify(`Applied spacing token: ${tokenName}`)
}
function getFontStyle(weight: number): string {
const styles = {
100: 'Thin',
200: 'Extra Light',
300: 'Light',
400: 'Regular',
500: 'Medium',
600: 'Semi Bold',
700: 'Bold',
800: 'Extra Bold',
900: 'Black'
}
return styles[weight] || 'Regular'
}
// Component Creation
async function createDesignComponent(component: Component) {
// Create component based on type
switch (component.category) {
case 'button':
await createButtonComponent(component)
break
case 'input':
await createInputComponent(component)
break
case 'card':
await createCardComponent(component)
break
case 'avatar':
await createAvatarComponent(component)
break
default:
await createGenericComponent(component)
}
}
async function createButtonComponent(component: Component) {
const designSystem = loadDesignSystem()
// Create button group
const buttonGroup = figma.createFrame()
buttonGroup.name = component.name
buttonGroup.resize(component.properties.width || 120, component.properties.height || 40)
// Apply button styles
const primaryToken = designSystem.tokens.find(t => t.name === 'primary-blue')
if (primaryToken) {
buttonGroup.fills = [{
type: 'SOLID',
color: primaryToken.value
}]
}
const borderRadiusToken = designSystem.tokens.find(t => t.name === 'border-radius-md')
if (borderRadiusToken) {
buttonGroup.cornerRadius = borderRadiusToken.value
}
// Add text label
const textNode = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' })
textNode.characters = component.properties.text || 'Button'
textNode.fontSize = 14
textNode.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]
// Center text in button
textNode.x = (buttonGroup.width - textNode.width) / 2
textNode.y = (buttonGroup.height - textNode.height) / 2
buttonGroup.appendChild(textNode)
figma.currentPage.appendChild(buttonGroup)
// Convert to component
const componentNode = buttonGroup.createComponent()
figma.currentPage.selection = [componentNode]
figma.notify(`Created button component: ${component.name}`)
}
async function createInputComponent(component: Component) {
const designSystem = loadDesignSystem()
// Create input frame
const inputFrame = figma.createFrame()
inputFrame.name = component.name
inputFrame.resize(component.properties.width || 200, component.properties.height || 40)
// Apply input styles
inputFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]
inputFrame.strokes = [{
type: 'SOLID',
color: { r: 0.8, g: 0.8, b: 0.8 }
}]
const borderRadiusToken = designSystem.tokens.find(t => t.name === 'border-radius-sm')
if (borderRadiusToken) {
inputFrame.cornerRadius = borderRadiusToken.value
}
// Add placeholder text
const placeholderText = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
placeholderText.characters = component.properties.placeholder || 'Enter text...'
placeholderText.fontSize = 14
placeholderText.fills = [{ type: 'SOLID', color: { r: 0.6, g: 0.6, b: 0.6 } }]
placeholderText.x = 12
placeholderText.y = (inputFrame.height - placeholderText.height) / 2
inputFrame.appendChild(placeholderText)
figma.currentPage.appendChild(inputFrame)
figma.notify(`Created input component: ${component.name}`)
}
async function createCardComponent(component: Component) {
const designSystem = loadDesignSystem()
// Create card frame
const cardFrame = figma.createFrame()
cardFrame.name = component.name
cardFrame.resize(component.properties.width || 300, component.properties.height || 200)
// Apply card styles
cardFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]
const shadowToken = designSystem.tokens.find(t => t.name === 'shadow-sm')
if (shadowToken) {
cardFrame.effects = [shadowToken.value]
}
const borderRadiusToken = designSystem.tokens.find(t => t.name === 'border-radius-md')
if (borderRadiusToken) {
cardFrame.cornerRadius = borderRadiusToken.value
}
figma.currentPage.appendChild(cardFrame)
figma.notify(`Created card component: ${component.name}`)
}
async function createAvatarComponent(component: Component) {
const avatarFrame = figma.createFrame()
avatarFrame.name = component.name
const size = component.properties.size || 40
avatarFrame.resize(size, size)
// Make circular
avatarFrame.cornerRadius = size / 2
// Random color or specified color
const color = component.properties.color || { r: 0.7, g: 0.7, b: 0.7 }
avatarFrame.fills = [{ type: 'SOLID', color }]
figma.currentPage.appendChild(avatarFrame)
figma.notify(`Created avatar component: ${component.name}`)
}
async function createGenericComponent(component: Component) {
const frame = figma.createFrame()
frame.name = component.name
if (component.properties.width) frame.resize(component.properties.width, component.properties.height || 100)
figma.currentPage.appendChild(frame)
figma.notify(`Created component: ${component.name}`)
}
// Style Guide Generation
async function generateStyleGuide(options: any) {
const designSystem = loadDesignSystem()
// Create style guide page
const styleGuidePage = figma.createPage()
styleGuidePage.name = `${designSystem.name} Style Guide`
let currentY = 50
// Title
const titleText = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' })
titleText.characters = designSystem.name
titleText.fontSize = 48
titleText.x = 50
titleText.y = currentY
styleGuidePage.appendChild(titleText)
currentY += 100
// Description
const descText = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
descText.characters = designSystem.description
descText.fontSize = 16
descText.x = 50
descText.y = currentY
styleGuidePage.appendChild(descText)
currentY += 80
// Generate sections for each token category
const categories = ['color', 'typography', 'spacing', 'effects', 'border']
for (const category of categories) {
const categoryTokens = designSystem.tokens.filter(t => t.category === category)
if (categoryTokens.length === 0) continue
// Section title
const sectionTitle = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Semi Bold' })
sectionTitle.characters = category.charAt(0).toUpperCase() + category.slice(1) + ' Tokens'
sectionTitle.fontSize = 24
sectionTitle.x = 50
sectionTitle.y = currentY
styleGuidePage.appendChild(sectionTitle)
currentY += 50
// Create token displays
for (const token of categoryTokens) {
await createTokenDisplay(token, currentY, styleGuidePage)
currentY += 60
}
currentY += 40
}
// Navigate to style guide page
figma.currentPage = styleGuidePage
figma.notify('Style guide generated successfully')
}
async function createTokenDisplay(token: DesignToken, y: number, page: PageNode) {
switch (token.category) {
case 'color':
await createColorTokenDisplay(token, y, page)
break
case 'typography':
await createTypographyTokenDisplay(token, y, page)
break
case 'spacing':
await createSpacingTokenDisplay(token, y, page)
break
default:
await createGenericTokenDisplay(token, y, page)
}
}
async function createColorTokenDisplay(token: DesignToken, y: number, page: PageNode) {
// Color swatch
const swatch = figma.createRectangle()
swatch.resize(60, 60)
swatch.x = 50
swatch.y = y
swatch.fills = [{ type: 'SOLID', color: token.value }]
page.appendChild(swatch)
// Token name
const nameText = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' })
nameText.characters = token.name
nameText.fontSize = 14
nameText.x = 130
nameText.y = y
page.appendChild(nameText)
// Token value
const valueText = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
valueText.characters = `RGB(${Math.round(token.value.r * 255)}, ${Math.round(token.value.g * 255)}, ${Math.round(token.value.b * 255)})`
valueText.fontSize = 12
valueText.fills = [{ type: 'SOLID', color: { r: 0.6, g: 0.6, b: 0.6 } }]
valueText.x = 130
valueText.y = y + 20
page.appendChild(valueText)
// Description
if (token.description) {
const descText = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
descText.characters = token.description
descText.fontSize = 12
descText.fills = [{ type: 'SOLID', color: { r: 0.4, g: 0.4, b: 0.4 } }]
descText.x = 130
descText.y = y + 40
page.appendChild(descText)
}
}
async function createTypographyTokenDisplay(token: DesignToken, y: number, page: PageNode) {
const sampleText = figma.createText()
await figma.loadFontAsync({
family: token.value.fontFamily,
style: getFontStyle(token.value.fontWeight)
})
sampleText.characters = 'The quick brown fox'
sampleText.fontSize = token.value.fontSize
sampleText.x = 50
sampleText.y = y
page.appendChild(sampleText)
// Token info
const infoText = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
infoText.characters = `${token.name} - ${token.value.fontFamily} ${token.value.fontWeight}`
infoText.fontSize = 12
infoText.fills = [{ type: 'SOLID', color: { r: 0.6, g: 0.6, b: 0.6 } }]
infoText.x = 50
infoText.y = y + token.value.fontSize + 10
page.appendChild(infoText)
}
async function createSpacingTokenDisplay(token: DesignToken, y: number, page: PageNode) {
const spacingBox = figma.createRectangle()
spacingBox.resize(token.value, token.value)
spacingBox.x = 50
spacingBox.y = y
spacingBox.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }]
page.appendChild(spacingBox)
// Token info
const infoText = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
infoText.characters = `${token.name}: ${token.value}px`
infoText.fontSize = 14
infoText.x = token.value + 70
infoText.y = y + token.value / 2 - 7
page.appendChild(infoText)
}
async function createGenericTokenDisplay(token: DesignToken, y: number, page: PageNode) {
const infoText = figma.createText()
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' })
infoText.characters = `${token.name}: ${JSON.stringify(token.value)}`
infoText.fontSize = 14
infoText.x = 50
infoText.y = y
page.appendChild(infoText)
}
// Token Extraction
async function extractTokensFromSelection() {
if (figma.currentPage.selection.length === 0) {
figma.notify('Please select objects to extract tokens from')
return
}
const extractedTokens: DesignToken[] = []
for (const node of figma.currentPage.selection) {
// Extract color tokens
if ('fills' in node) {
for (const fill of node.fills) {
if (fill.type === 'SOLID') {
const token: DesignToken = {
name: `${node.name.toLowerCase().replace(/\s+/g, '-')}-color`,
category: 'color',
value: fill.color,
description: `Extracted from ${node.name}`
}
extractedTokens.push(token)
}
}
}
// Extract typography tokens
if (node.type === 'TEXT') {
const textNode = node as TextNode
const token: DesignToken = {
name: `${node.name.toLowerCase().replace(/\s+/g, '-')}-typography`,
category: 'typography',
value: {
fontFamily: textNode.fontName.family,
fontWeight: getFontWeight(textNode.fontName.style),
fontSize: textNode.fontSize,
lineHeight: textNode.lineHeight?.value || textNode.fontSize * 1.4
},
description: `Extracted from ${node.name}`
}
extractedTokens.push(token)
}
// Extract spacing tokens
if ('paddingTop' in node) {
const frameNode = node as FrameNode
const token: DesignToken = {
name: `${node.name.toLowerCase().replace(/\s+/g, '-')}-padding`,
category: 'spacing',
value: frameNode.paddingTop,
description: `Extracted from ${node.name}`
}
extractedTokens.push(token)
}
}
// Send extracted tokens to UI
figma.ui.postMessage({
type: 'tokens-extracted',
tokens: extractedTokens
})
figma.notify(`Extracted ${extractedTokens.length} tokens`)
}
function getFontWeight(style: string): number {
const weights: Record<string, number> = {
'Thin': 100,
'Extra Light': 200,
'Light': 300,
'Regular': 400,
'Medium': 500,
'Semi Bold': 600,
'Bold': 700,
'Extra Bold': 800,
'Black': 900
}
return weights[style] || 400
}
// Design System Audit
async function auditDesignSystem() {
const designSystem = loadDesignSystem()
const issues = []
// Audit all nodes in current page
const allNodes = figma.currentPage.findAll(node => true)
for (const node of allNodes) {
// Check for inconsistent colors
if ('fills' in node) {
for (const fill of node.fills) {
if (fill.type === 'SOLID') {
const matchingToken = designSystem.tokens.find(t =>
t.category === 'color' &&
JSON.stringify(t.value) === JSON.stringify(fill.color)
)
if (!matchingToken) {
issues.push({
type: 'inconsistent-color',
node: node.name,
color: fill.color
})
}
}
}
}
// Check for inconsistent typography
if (node.type === 'TEXT') {
const textNode = node as TextNode
const matchingToken = designSystem.tokens.find(t =>
t.category === 'typography' &&
t.value.fontFamily === textNode.fontName.family &&
t.value.fontSize === textNode.fontSize
)
if (!matchingToken) {
issues.push({
type: 'inconsistent-typography',
node: node.name,
fontFamily: textNode.fontName.family,
fontSize: textNode.fontSize
})
}
}
}
// Send audit results to UI
figma.ui.postMessage({
type: 'audit-complete',
issues
})
figma.notify(`Audit complete. Found ${issues.length} issues`)
}
// Library Sync (placeholder)
async function syncWithDesignLibrary(libraryId: string) {
figma.notify('Library sync functionality would be implemented here')
// This would integrate with Figma's team libraries API
}
💻 Инструменты Автоматизации Дизайна typescript
🔴 complex
⭐⭐⭐⭐⭐
Продвинутые инструменты автоматизации для пакетной обработки, очистки дизайна и оптимизации рабочих процессов
⏱️ 50 min
🏷️ figma, plugin, typescript, automation, batch
Prerequisites:
Expert TypeScript, Figma API mastery, Automation concepts
// Figma Design Automation Tools
// Batch processing, design cleanup, and workflow optimization
interface AutomationRule {
name: string
description: string
category: 'cleanup' | 'organization' | 'naming' | 'accessibility' | 'performance'
enabled: boolean
action: Function
}
interface BatchOperation {
id: string
name: string
description: string
operation: Function
progress: number
status: 'pending' | 'running' | 'completed' | 'error'
}
// Main plugin code
export default function () {
showUI({
height: 700,
width: 450
}, {
rules: getAutomationRules(),
operations: getBatchOperations(),
pageStats: getPageStatistics()
})
}
// Message handling
figma.ui.onmessage = async (msg: any) => {
switch (msg.type) {
case 'run-automation':
await runAutomationRules(msg.rules, msg.scope)
break
case 'batch-operation':
await executeBatchOperation(msg.operationId, msg.options)
break
case 'generate-report':
await generateDesignReport(msg.options)
break
case 'optimize-performance':
await optimizePerformance()
break
case 'check-accessibility':
await checkAccessibility()
break
case 'export-assets':
await exportAssets(msg.options)
break
case 'rename-nodes':
await batchRenameNodes(msg.pattern, msg.replacement)
break
}
}
// Automation Rules
function getAutomationRules(): AutomationRule[] {
return [
{
name: 'Remove Unused Layers',
description: 'Remove hidden and empty layers to reduce file size',
category: 'cleanup',
enabled: true,
action: removeUnusedLayers
},
{
name: 'Standardize Naming',
description: 'Rename layers to follow consistent naming conventions',
category: 'naming',
enabled: true,
action: standardizeNaming
},
{
name: 'Group Similar Elements',
description: 'Group related elements together for better organization',
category: 'organization',
enabled: false,
action: groupSimilarElements
},
{
name: 'Check Color Contrast',
description: 'Verify text elements meet accessibility contrast ratios',
category: 'accessibility',
enabled: true,
action: checkColorContrast
},
{
name: 'Optimize Images',
description: 'Compress and optimize images for better performance',
category: 'performance',
enabled: false,
action: optimizeImages
},
{
name: 'Remove Duplicate Styles',
description: 'Find and remove duplicate text and color styles',
category: 'cleanup',
enabled: true,
action: removeDuplicateStyles
}
]
}
// Cleanup Functions
async function removeUnusedLayers(scope: 'page' | 'selection' = 'page') {
const nodes = scope === 'page' ? figma.currentPage.findAll(n => true) : figma.currentPage.selection
const removedCount = 0
for (const node of nodes) {
let shouldRemove = false
// Check if node is hidden and has no children
if (!node.visible && node.type !== 'PAGE') {
shouldRemove = true
}
// Check if frame is empty
if (node.type === 'FRAME' && node.children.length === 0) {
shouldRemove = true
}
// Check if layer is completely outside canvas bounds
if (node.x > figma.viewport.bounds.width + 1000 ||
node.y > figma.viewport.bounds.height + 1000 ||
node.x + node.width < -1000 ||
node.y + node.height < -1000) {
shouldRemove = true
}
if (shouldRemove) {
node.remove()
removedCount++
}
}
figma.notify(`Removed ${removedCount} unused layers`)
return removedCount
}
async function standardizeNaming(scope: 'page' | 'selection' = 'page') {
const nodes = scope === 'page' ? figma.currentPage.findAll(n => true) : figma.currentPage.selection
let renamedCount = 0
for (const node of nodes) {
if (node.type === 'PAGE') continue
const originalName = node.name
let newName = originalName
// Standardize naming patterns
newName = newName.replace(/\s+/g, '-') // Replace spaces with hyphens
newName = newName.replace(/[^a-zA-Z0-9-_]/g, '') // Remove special characters
newName = newName.toLowerCase() // Convert to lowercase
// Add prefixes based on type
if (!newName.match(/^[a-z]+-/)) {
const prefix = getNodeTypePrefix(node.type)
newName = `${prefix}-${newName}`
}
// Remove multiple consecutive hyphens
newName = newName.replace(/-+/g, '-')
// Remove trailing hyphens
newName = newName.replace(/-+$/, '')
if (newName !== originalName && newName !== '') {
node.name = newName
renamedCount++
}
}
figma.notify(`Renamed ${renamedCount} layers`)
return renamedCount
}
function getNodeTypePrefix(type: NodeType): string {
const prefixes = {
'FRAME': 'frame',
'GROUP': 'group',
'COMPONENT': 'component',
'INSTANCE': 'instance',
'TEXT': 'text',
'RECTANGLE': 'rect',
'ELLIPSE': 'ellipse',
'POLYGON': 'polygon',
'STAR': 'star',
'VECTOR': 'vector',
'LINE': 'line'
}
return prefixes[type] || 'layer'
}
async function groupSimilarElements(scope: 'page' | 'selection' = 'page') {
const nodes = scope === 'page' ? figma.currentPage.findAll(n => true) : figma.currentPage.selection
const groups = new Map<string, SceneNode[]>()
// Group elements by type and similar properties
for (const node of nodes) {
if (node.type === 'PAGE') continue
let groupKey = node.type
// Additional grouping criteria
if (node.type === 'TEXT') {
const textNode = node as TextNode
groupKey += `-${textNode.fontSize}-${textNode.fontName.family}`
}
if ('fills' in node && node.fills.length > 0) {
const fill = node.fills[0]
if (fill.type === 'SOLID') {
groupKey += `-${Math.round(fill.color.r * 255)}-${Math.round(fill.color.g * 255)}-${Math.round(fill.color.b * 255)}`
}
}
if (!groups.has(groupKey)) {
groups.set(groupKey, [])
}
groups.get(groupKey)!.push(node)
}
// Create groups for similar elements
let groupCount = 0
for (const [key, elements] of groups) {
if (elements.length > 1) {
const groupFrame = figma.createFrame()
groupFrame.name = `group-${key.toLowerCase().replace(/[^a-zA-Z0-9]/g, '-')}`
// Calculate group bounds
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
for (const element of elements) {
minX = Math.min(minX, element.x)
minY = Math.min(minY, element.y)
maxX = Math.max(maxX, element.x + element.width)
maxY = Math.max(maxY, element.y + element.height)
}
// Position and size the group
groupFrame.x = minX - 20
groupFrame.y = minY - 20
groupFrame.resize(maxX - minX + 40, maxY - minY + 40)
// Add elements to group
for (const element of elements) {
groupFrame.appendChild(element)
}
figma.currentPage.appendChild(groupFrame)
groupCount++
}
}
figma.notify(`Created ${groupCount} groups`)
return groupCount
}
// Accessibility Functions
async function checkColorContrast(scope: 'page' | 'selection' = 'page') {
const nodes = scope === 'page' ? figma.currentPage.findAll(n => n.type === 'TEXT') :
figma.currentPage.selection.filter(n => n.type === 'TEXT')
const issues = []
for (const node of nodes) {
const textNode = node as TextNode
const backgroundColor = getBackgroundColor(textNode)
if (backgroundColor) {
const contrastRatio = calculateContrastRatio(
textNode.fills[0]?.color || { r: 0, g: 0, b: 0 },
backgroundColor
)
const isLargeText = textNode.fontSize >= 18
const minimumRatio = isLargeText ? 3.0 : 4.5
if (contrastRatio < minimumRatio) {
issues.push({
node: textNode.name,
contrast: contrastRatio,
minimum: minimumRatio,
fontSize: textNode.fontSize
})
}
}
}
figma.ui.postMessage({
type: 'accessibility-report',
issues
})
figma.notify(`Found ${issues.length} contrast issues`)
return issues
}
function getBackgroundColor(textNode: TextNode): Color | null {
let current: SceneNode | null = textNode
while (current) {
if ('fills' in current && current.fills.length > 0) {
const fill = current.fills[0]
if (fill.type === 'SOLID') {
return fill.color
}
}
current = current.parent
}
return null
}
function calculateContrastRatio(color1: Color, color2: Color): number {
const luminance1 = calculateLuminance(color1)
const luminance2 = calculateLuminance(color2)
const lighter = Math.max(luminance1, luminance2)
const darker = Math.min(luminance1, luminance2)
return (lighter + 0.05) / (darker + 0.05)
}
function calculateLuminance(color: Color): number {
const rsRGB = color.r
const gsRGB = color.g
const bsRGB = color.b
const r = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4)
const g = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4)
const b = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
}
// Performance Functions
async function optimizeImages() {
const imageNodes = figma.currentPage.findAll(n =>
('fills' in n) && n.fills.some(fill => fill.type === 'IMAGE')
)
let optimizedCount = 0
for (const node of imageNodes) {
if ('fills' in node) {
const newFills = [...node.fills]
for (let i = 0; i < newFills.length; i++) {
if (newFills[i].type === 'IMAGE') {
// In a real implementation, you would compress the image here
// For now, we'll just mark it as optimized
optimizedCount++
}
}
node.fills = newFills
}
}
figma.notify(`Optimized ${optimizedCount} images`)
return optimizedCount
}
async function removeDuplicateStyles() {
const allTextNodes = figma.currentPage.findAll(n => n.type === 'TEXT') as TextNode[]
const styles = new Map<string, TextNode[]>()
// Group similar text styles
for (const node of allTextNodes) {
const styleKey = getTextStyleKey(node)
if (!styles.has(styleKey)) {
styles.set(styleKey, [])
}
styles.get(styleKey)!.push(node)
}
let duplicateCount = 0
for (const [styleKey, nodes] of styles) {
if (nodes.length > 1) {
// Create a shared style for duplicates
const firstNode = nodes[0]
// Check if style already exists
const existingStyle = figma.getLocalTextStyles().find(style =>
style.fontName.family === firstNode.fontName.family &&
style.fontName.style === firstNode.fontName.style &&
style.fontSize === firstNode.fontSize
)
if (!existingStyle) {
const newStyle = figma.createTextStyle()
newStyle.name = `Style-${styleKey}`
newStyle.fontName = firstNode.fontName
newStyle.fontSize = firstNode.fontSize
newStyle.letterSpacing = firstNode.letterSpacing
newStyle.lineHeight = firstNode.lineHeight
newStyle.paragraphIndent = firstNode.paragraphIndent
newStyle.paragraphSpacing = firstNode.paragraphSpacing
newStyle.textCase = firstNode.textCase
newStyle.textDecoration = firstNode.textDecoration
// Apply style to all nodes with this style
for (const node of nodes) {
node.textStyleId = newStyle.id
}
duplicateCount += nodes.length - 1
}
}
}
figma.notify(`Consolidated ${duplicateCount} duplicate styles`)
return duplicateCount
}
function getTextStyleKey(node: TextNode): string {
return `${node.fontName.family}-${node.fontName.style}-${node.fontSize}-${node.lineHeight?.value || 'auto'}`
}
// Batch Operations
function getBatchOperations(): BatchOperation[] {
return [
{
id: 'export-components',
name: 'Export All Components',
description: 'Export all components as SVG files',
operation: exportAllComponents,
progress: 0,
status: 'pending'
},
{
id: 'resize-artboards',
name: 'Resize All Artboards',
description: 'Resize all artboards to specified dimensions',
operation: resizeAllArtboards,
progress: 0,
status: 'pending'
},
{
id: 'generate-thumbnails',
name: 'Generate Thumbnails',
description: 'Create thumbnail previews for all frames',
operation: generateThumbnails,
progress: 0,
status: 'pending'
}
]
}
async function executeBatchOperation(operationId: string, options: any) {
const operations = getBatchOperations()
const operation = operations.find(op => op.id === operationId)
if (!operation) {
figma.notify('Operation not found')
return
}
operation.status = 'running'
figma.ui.postMessage({
type: 'operation-status',
operationId,
status: 'running'
})
try {
await operation.operation(options)
operation.status = 'completed'
operation.progress = 100
figma.ui.postMessage({
type: 'operation-status',
operationId,
status: 'completed',
progress: 100
})
figma.notify(`Completed: ${operation.name}`)
} catch (error) {
operation.status = 'error'
figma.ui.postMessage({
type: 'operation-status',
operationId,
status: 'error',
error: error.message
})
figma.notify(`Error in ${operation.name}: ${error.message}`)
}
}
// Batch Operation Implementations
async function exportAllComponents(options: any) {
const components = figma.currentPage.findAll(n => n.type === 'COMPONENT')
for (let i = 0; i < components.length; i++) {
const component = components[i]
if ('exportAsync' in component) {
const bytes = await component.exportAsync({
format: 'SVG',
contentsOnly: true
})
figma.ui.postMessage({
type: 'export-progress',
operationId: 'export-components',
progress: (i + 1) / components.length * 100,
file: {
name: `${component.name}.svg`,
data: Array.from(bytes)
}
})
}
}
}
async function resizeAllArtboards(options: { width: number; height: number }) {
const frames = figma.currentPage.findAll(n => n.type === 'FRAME')
for (let i = 0; i < frames.length; i++) {
const frame = frames[i] as FrameNode
frame.resize(options.width, options.height)
figma.ui.postMessage({
type: 'export-progress',
operationId: 'resize-artboards',
progress: (i + 1) / frames.length * 100
})
}
}
async function generateThumbnails(options: any) {
const frames = figma.currentPage.findAll(n => n.type === 'FRAME')
for (let i = 0; i < frames.length; i++) {
const frame = frames[i]
if ('exportAsync' in frame) {
const bytes = await frame.exportAsync({
format: 'PNG',
constraint: { type: 'SCALE', value: 0.2 }
})
figma.ui.postMessage({
type: 'export-progress',
operationId: 'generate-thumbnails',
progress: (i + 1) / frames.length * 100,
file: {
name: `${frame.name}-thumb.png`,
data: Array.from(bytes)
}
})
}
}
}
// Reporting Functions
async function generateDesignReport(options: any) {
const report = {
page: {
name: figma.currentPage.name,
nodeCount: figma.currentPage.findAll(n => true).length,
componentCount: figma.currentPage.findAll(n => n.type === 'COMPONENT').length,
frameCount: figma.currentPage.findAll(n => n.type === 'FRAME').length
},
styles: {
textStyles: figma.getLocalTextStyles().length,
colorStyles: figma.getLocalPaintStyles().length,
effectStyles: figma.getLocalEffectStyles().length
},
issues: []
}
// Analyze potential issues
report.issues = await analyzeDesignIssues()
figma.ui.postMessage({
type: 'report-generated',
report
})
}
async function analyzeDesignIssues(): Promise<any[]> {
const issues = []
const allNodes = figma.currentPage.findAll(n => true)
// Check for very large images
for (const node of allNodes) {
if ('fills' in node) {
for (const fill of node.fills) {
if (fill.type === 'IMAGE' && fill.imageHash) {
// Check image size (simplified)
if (node.width * node.height > 4000000) { // > 4MP
issues.push({
type: 'large-image',
node: node.name,
size: node.width * node.height,
recommendation: 'Consider compressing this image'
})
}
}
}
}
}
// Check for deep nesting
const maxDepth = findMaxDepth(figma.currentPage)
if (maxDepth > 10) {
issues.push({
type: 'deep-nesting',
depth: maxDepth,
recommendation: 'Consider flattening the structure to improve performance'
})
}
// Check for very small text
const smallTexts = figma.currentPage.findAll(n =>
n.type === 'TEXT' && (n as TextNode).fontSize < 8
)
if (smallTexts.length > 0) {
issues.push({
type: 'small-text',
count: smallTexts.length,
recommendation: 'Very small text may be hard to read'
})
}
return issues
}
function findMaxDepth(node: SceneNode, currentDepth = 0): number {
if ('children' in node) {
let maxChildDepth = currentDepth
for (const child of node.children) {
maxChildDepth = Math.max(maxChildDepth, findMaxDepth(child, currentDepth + 1))
}
return maxChildDepth
}
return currentDepth
}
// Utility Functions
function getPageStatistics() {
return {
nodeCount: figma.currentPage.findAll(n => true).length,
componentCount: figma.currentPage.findAll(n => n.type === 'COMPONENT').length,
textCount: figma.currentPage.findAll(n => n.type === 'TEXT').length,
frameCount: figma.currentPage.findAll(n => n.type === 'FRAME').length,
imageCount: figma.currentPage.findAll(n =>
('fills' in n) && n.fills.some(f => f.type === 'IMAGE')
).length
}
}
// Additional utility functions
async function batchRenameNodes(pattern: string, replacement: string) {
const nodes = figma.currentPage.selection.length > 0 ?
figma.currentPage.selection : figma.currentPage.findAll(n => true)
let renamedCount = 0
for (const node of nodes) {
if (node.type === 'PAGE') continue
const oldName = node.name
const newName = oldName.replace(new RegExp(pattern, 'g'), replacement)
if (newName !== oldName) {
node.name = newName
renamedCount++
}
}
figma.notify(`Renamed ${renamedCount} nodes`)
}
async function exportAssets(options: any) {
const selection = figma.currentPage.selection.length > 0 ?
figma.currentPage.selection : figma.currentPage.findAll(n => true)
for (const node of selection) {
if ('exportAsync' in node) {
const bytes = await node.exportAsync(options.exportSettings)
figma.ui.postMessage({
type: 'asset-exported',
asset: {
name: node.name,
data: Array.from(bytes)
}
})
}
}
}