🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples de Plugins Figma
Exemples de développement de plugins Figma avec création d'UI, automatisation de conception et outils d'amélioration de flux de travail
💻 Plugin Figma Hello World typescript
🟢 simple
⭐⭐
Configuration de base de plugin Figma avec création d'UI, interaction API et fondamentaux de passage de messages
⏱️ 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>
*/
💻 Gestionnaire de Système de Design typescript
🟡 intermediate
⭐⭐⭐⭐
Plugin complet de système de design avec bibliothèque de composants, gestion des styles et système de tokens
⏱️ 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
}
💻 Outils d'Automatisation de Design typescript
🔴 complex
⭐⭐⭐⭐⭐
Outils avancés d'automatisation pour le traitement par lots, le nettoyage de design et l'optimisation des flux de travail
⏱️ 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)
}
})
}
}
}