693 lines
36 KiB
HTML
693 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en" data-theme="dark" data-radius="medium" data-font="merriweather">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||
<meta
|
||
name="description"
|
||
content="Case study of a Figma plugin that retrieves and runs scripts from Cursor."
|
||
/>
|
||
<title>AI assistant: a script bridge between Cursor and Figma</title>
|
||
<script>
|
||
(function () {
|
||
var FONT_CSS = {
|
||
merriweather: "/assets/fonts/merriweather.css",
|
||
geologica: "/assets/fonts/geologica.css",
|
||
mono: "/assets/fonts/mono.css",
|
||
};
|
||
try {
|
||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||
var d = document.documentElement;
|
||
var m = t.mode;
|
||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||
var eff =
|
||
m === "system"
|
||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||
? "dark"
|
||
: "light"
|
||
: m;
|
||
d.dataset.theme = eff;
|
||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||
if (l !== null) {
|
||
var L = Number(l);
|
||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||
}
|
||
var hVal = h !== null ? h : 210;
|
||
var sVal = s !== null ? s : 90;
|
||
var lVal = l !== null ? Math.min(70, Math.max(20, Number(l))) : 40;
|
||
var H = (((hVal % 360) + 360) % 360) / 360;
|
||
var S = Math.min(100, Math.max(0, sVal)) / 100;
|
||
var Lv = Math.min(100, Math.max(0, lVal)) / 100;
|
||
var r, g, b;
|
||
if (S === 0) { r = g = b = Lv; }
|
||
else {
|
||
var q = Lv < 0.5 ? Lv * (1 + S) : Lv + S - Lv * S;
|
||
var p = 2 * Lv - q;
|
||
var hue2rgb = function(p, q, t) {
|
||
if (t < 0) t += 1; if (t > 1) t -= 1;
|
||
if (t < 1/6) return p + (q - p) * 6 * t;
|
||
if (t < 1/2) return q;
|
||
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||
return p;
|
||
};
|
||
r = hue2rgb(p, q, H + 1/3);
|
||
g = hue2rgb(p, q, H);
|
||
b = hue2rgb(p, q, H - 1/3);
|
||
}
|
||
var lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||
d.style.setProperty("--on-accent", lum > 0.58 ? "hsl(220 14% 12%)" : "hsl(0 0% 98%)");
|
||
var rad = t.radius;
|
||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||
d.dataset.radius = rad;
|
||
} else {
|
||
d.dataset.radius = "medium";
|
||
}
|
||
var f = t.font;
|
||
if (f === "onest") f = "geologica";
|
||
if (f === "anonymous") f = "mono";
|
||
if (f !== "merriweather" && f !== "mono" && f !== "geologica") f = "merriweather";
|
||
d.dataset.font = f;
|
||
var mb = t.meshBg;
|
||
if (mb === "off") {
|
||
d.dataset.meshBg = "off";
|
||
} else {
|
||
d.dataset.meshBg = "on";
|
||
}
|
||
var href = FONT_CSS[f];
|
||
if (href && !document.querySelector('link[data-theme-font="' + f + '"]')) {
|
||
var link = document.createElement("link");
|
||
link.rel = "stylesheet";
|
||
link.href = href;
|
||
link.setAttribute("data-theme-font", f);
|
||
document.head.appendChild(link);
|
||
}
|
||
} catch (e) {}
|
||
})();
|
||
</script>
|
||
|
||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon/favicon-32x32.png" />
|
||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png" />
|
||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png" />
|
||
<link rel="manifest" href="/assets/favicon/site.webmanifest" />
|
||
<link rel="preload" href="/assets/fonts/merriweather.css" as="style" />
|
||
<link rel="preload" href="/css/main.css" as="style" />
|
||
<link rel="preload" href="/js/skill-connectors.js" as="script" />
|
||
<link rel="modulepreload" href="/js/main.js" />
|
||
<link rel="modulepreload" href="/js/modules/ui/page-mesh-shader.js" />
|
||
<link rel="stylesheet" href="/css/main.css" />
|
||
<link rel="canonical" href="https://mamadaevv.site/en/cases/ai-assistant-figma/" />
|
||
<link rel="alternate" hreflang="ru" href="https://mamadaevv.site/cases/ai-assistant-figma/" />
|
||
<link rel="alternate" hreflang="en" href="https://mamadaevv.site/en/cases/ai-assistant-figma/" />
|
||
<link rel="alternate" hreflang="x-default" href="https://mamadaevv.site/cases/ai-assistant-figma/" />
|
||
<meta name="author" content="Nikolai Mamadaev" />
|
||
<meta name="robots" content="index, follow" />
|
||
<meta name="theme-color" content="#0f1115" />
|
||
|
||
<!-- Open Graph -->
|
||
<meta property="og:type" content="article" />
|
||
<meta property="og:site_name" content="Nikolai Mamadaev — Product Designer" />
|
||
<meta property="og:locale" content="en_US" />
|
||
<meta property="og:locale:alternate" content="ru_RU" />
|
||
<meta property="og:title" content="AI assistant: a script bridge between Cursor and Figma" />
|
||
<meta property="og:description" content="Case study of a Figma plugin that retrieves and runs scripts from Cursor." />
|
||
<meta property="og:url" content="https://mamadaevv.site/en/cases/ai-assistant-figma/" />
|
||
<meta property="og:image" content="https://mamadaevv.site/assets/og/og-default.png" />
|
||
<meta property="og:image:width" content="1200" />
|
||
<meta property="og:image:height" content="630" />
|
||
<meta property="og:image:alt" content="AI assistant: a script bridge between Cursor and Figma" />
|
||
|
||
<!-- Twitter / X -->
|
||
<meta name="twitter:card" content="summary_large_image" />
|
||
<meta name="twitter:title" content="AI assistant: a script bridge between Cursor and Figma" />
|
||
<meta name="twitter:description" content="Case study of a Figma plugin that retrieves and runs scripts from Cursor." />
|
||
<meta name="twitter:image" content="https://mamadaevv.site/assets/og/og-default.png" />
|
||
<meta name="twitter:image:alt" content="AI assistant: a script bridge between Cursor and Figma" />
|
||
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@graph": [
|
||
{
|
||
"@type": "Person",
|
||
"@id": "https://mamadaevv.site/#person",
|
||
"name": "Nikolai Mamadaev",
|
||
"url": "https://mamadaevv.site",
|
||
"jobTitle": "Product Designer",
|
||
"description": "Product designer with 10+ years of experience. Designing digital products, interfaces and design systems.",
|
||
"knowsAbout": [
|
||
"UX/UI design",
|
||
"product design",
|
||
"design systems",
|
||
"prototyping",
|
||
"Figma",
|
||
"no-code"
|
||
],
|
||
"sameAs": [
|
||
"https://t.me/mamadaevv",
|
||
"https://vk.com/mamadaevv",
|
||
"https://github.com/mamadaevv"
|
||
]
|
||
},
|
||
{
|
||
"@type": "WebSite",
|
||
"@id": "https://mamadaevv.site/#website",
|
||
"url": "https://mamadaevv.site/",
|
||
"name": "Nikolai Mamadaev — Product Designer",
|
||
"inLanguage": "en",
|
||
"author": {
|
||
"@id": "https://mamadaevv.site/#person"
|
||
},
|
||
"publisher": {
|
||
"@id": "https://mamadaevv.site/#person"
|
||
}
|
||
},
|
||
{
|
||
"@type": "CreativeWork",
|
||
"@id": "https://mamadaevv.site/en/cases/ai-assistant-figma/#page",
|
||
"url": "https://mamadaevv.site/en/cases/ai-assistant-figma/",
|
||
"name": "AI assistant: a script bridge between Cursor and Figma",
|
||
"description": "Case study of a Figma plugin that retrieves and runs scripts from Cursor.",
|
||
"isPartOf": {
|
||
"@id": "https://mamadaevv.site/#website"
|
||
},
|
||
"author": {
|
||
"@id": "https://mamadaevv.site/#person"
|
||
},
|
||
"inLanguage": "en",
|
||
"creator": {
|
||
"@id": "https://mamadaevv.site/#person"
|
||
},
|
||
"datePublished": "2026-09-26",
|
||
"about": "UX/UI design and interface design"
|
||
},
|
||
{
|
||
"@type": "BreadcrumbList",
|
||
"@id": "https://mamadaevv.site/en/cases/ai-assistant-figma/#breadcrumbs",
|
||
"itemListElement": [
|
||
{
|
||
"@type": "ListItem",
|
||
"position": 1,
|
||
"name": "Nikolai Mamadaev — Product Designer",
|
||
"item": "https://mamadaevv.site/"
|
||
},
|
||
{
|
||
"@type": "ListItem",
|
||
"position": 2,
|
||
"name": "Cases",
|
||
"item": "https://mamadaevv.site/cases/"
|
||
},
|
||
{
|
||
"@type": "ListItem",
|
||
"position": 3,
|
||
"name": "AI assistant: a script bridge between Cursor and Figma",
|
||
"item": "https://mamadaevv.site/cases/ai-assistant-figma/"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
</script>
|
||
|
||
<script src="/js/skill-connectors.js" defer></script>
|
||
<script type="module" src="/js/main.js"></script>
|
||
|
||
|
||
<link rel="stylesheet" href="/vendor/plyr/plyr.css" />
|
||
|
||
<style>
|
||
.case-article {
|
||
max-width: 48rem;
|
||
margin: 0 auto;
|
||
}
|
||
.case-article h2 {
|
||
margin: 2rem 0 0.75rem;
|
||
font-size: clamp(1.15rem, 2vw, 1.35rem);
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
}
|
||
.case-article h3 {
|
||
font-size: 1.05rem;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
.case-article .section__prose {
|
||
margin-bottom: 0.75rem;
|
||
color: var(--text);
|
||
}
|
||
.case-article a:not(.btn) {
|
||
color: var(--accent);
|
||
}
|
||
/* Реальный размер картинки: натуральная ширина в пикселях, ограниченная колонкой */
|
||
.case-media--real {
|
||
width: auto;
|
||
margin-inline: auto;
|
||
margin-left: auto;
|
||
transform: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
.case-media--real img {
|
||
width: auto;
|
||
max-width: 100%;
|
||
margin-inline: auto;
|
||
}
|
||
</style>
|
||
<script>
|
||
(function () {
|
||
try {
|
||
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
|
||
var d = document.documentElement;
|
||
var m = t.mode;
|
||
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
|
||
var eff =
|
||
m === "system"
|
||
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||
? "dark"
|
||
: "light"
|
||
: m;
|
||
d.dataset.theme = eff;
|
||
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
|
||
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
|
||
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
|
||
if (h !== null) d.style.setProperty("--theme-h", String(h));
|
||
if (s !== null) d.style.setProperty("--theme-s", String(s));
|
||
if (l !== null) {
|
||
var L = Number(l);
|
||
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
|
||
}
|
||
var rad = t.radius;
|
||
if (rad === "soft" || rad === "medium" || rad === "strong") {
|
||
d.dataset.radius = rad;
|
||
} else {
|
||
d.dataset.radius = "medium";
|
||
}
|
||
var f = t.font;
|
||
if (f === "onest") f = "geologica";
|
||
if (f === "anonymous") f = "mono";
|
||
if (f === "merriweather" || f === "mono" || f === "geologica") {
|
||
d.dataset.font = f;
|
||
} else {
|
||
d.dataset.font = "merriweather";
|
||
}
|
||
var mb = t.meshBg;
|
||
if (mb === "off") {
|
||
d.dataset.meshBg = "off";
|
||
} else {
|
||
d.dataset.meshBg = "on";
|
||
}
|
||
} catch (e) {}
|
||
})();
|
||
</script>
|
||
</head>
|
||
<body data-current-case="ai-assistant">
|
||
<svg class="liquid-glass-defs" aria-hidden="true" width="0" height="0" focusable="false" style="position:absolute;width:0;height:0;overflow:hidden;pointer-events:none">
|
||
<filter id="liquid-glass-displacement" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
|
||
<feImage href="../../../assets/liquid-glass/displacement.png" xlink:href="../../../assets/liquid-glass/displacement.png" preserveAspectRatio="none" result="lgMap"/>
|
||
<feDisplacementMap in="SourceGraphic" in2="lgMap" scale="14" xChannelSelector="R" yChannelSelector="G"/>
|
||
</filter>
|
||
<filter id="liquid-glass-displacement-btn" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
|
||
<feImage href="../../../assets/liquid-glass/displacement.png" xlink:href="../../../assets/liquid-glass/displacement.png" preserveAspectRatio="none" result="lgMapBtn"/>
|
||
<feDisplacementMap in="SourceGraphic" in2="lgMapBtn" scale="6" xChannelSelector="R" yChannelSelector="G"/>
|
||
</filter>
|
||
</svg>
|
||
|
||
<a class="skip-link" href="#case-main">Skip to case content</a>
|
||
<header class="header" data-theme-picker>
|
||
<div class="header__inner">
|
||
<a class="logo" href="../../" aria-label="Home">NM</a>
|
||
<div class="header__cluster">
|
||
<nav class="nav" aria-label="Page sections">
|
||
<a href="#contact">Contact</a>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="fab-stack" aria-label="Quick actions">
|
||
<div class="theme-panel-fab-wrap">
|
||
<button type="button" class="fab fab--secondary glass-neutral" data-theme-panel-btn aria-expanded="false" aria-controls="theme-panel" aria-label="Style settings">
|
||
<span class="theme-panel-fab__icon theme-panel-fab__icon--open-hide" aria-hidden="true"></span>
|
||
<svg class="theme-panel-fab__icon theme-panel-fab__icon--close" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||
<path d="M6 6L18 18M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||
</svg>
|
||
</button>
|
||
<div id="theme-panel" class="theme-picker__panel glass-neutral" role="dialog" aria-label="Theme & appearance" hidden data-theme-panel>
|
||
<div class="theme-picker__stack">
|
||
<section class="theme-picker__section" aria-labelledby="theme-accent-title">
|
||
<h3 id="theme-accent-title" class="theme-picker__section-title">Accent color</h3>
|
||
<div class="colorpicker" data-colorpicker>
|
||
<div class="colorpicker__plane-box">
|
||
<canvas class="colorpicker__plane" width="320" height="180" data-sl-canvas aria-label="Accent saturation and lightness"></canvas>
|
||
<div class="colorpicker__knob" data-sl-knob></div>
|
||
</div>
|
||
<div class="colorpicker__hue-wrap">
|
||
<label class="colorpicker__hue-sr" for="theme-hue-slider">Accent hue</label>
|
||
<input id="theme-hue-slider" class="colorpicker__hue" type="range" min="0" max="360" value="210" data-hue-slider aria-valuemin="0" aria-valuemax="360" />
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="theme-picker__section" aria-labelledby="theme-mode-title">
|
||
<h3 id="theme-mode-title" class="theme-picker__section-title">Theme</h3>
|
||
<div class="radius-seg theme-mode-seg" role="radiogroup" aria-labelledby="theme-mode-title">
|
||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="system" aria-label="System"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--system"></span></span></button>
|
||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="light" aria-label="Light"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--light"></span></span></button>
|
||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="dark" aria-label="Dark"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--dark"></span></span></button>
|
||
</div>
|
||
</section>
|
||
<section class="theme-picker__section" aria-labelledby="theme-radius-title">
|
||
<h3 id="theme-radius-title" class="theme-picker__section-title">Corner radius</h3>
|
||
<div class="radius-seg" role="radiogroup" aria-labelledby="theme-radius-title">
|
||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="soft" aria-label="Soft radius"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--soft"></span></span></button>
|
||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="medium" aria-label="Medium radius"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--medium"></span></span></button>
|
||
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="strong" aria-label="Strong radius"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--strong"></span></span></button>
|
||
</div>
|
||
</section>
|
||
<section class="theme-picker__section" aria-labelledby="theme-typography-title">
|
||
<h3 id="theme-typography-title" class="theme-picker__section-title">Typography</h3>
|
||
<div class="radius-seg" role="radiogroup" aria-labelledby="theme-typography-title">
|
||
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="geologica" aria-label="Sans"><span class="theme-font-seg__label">Sans</span></button>
|
||
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="merriweather" aria-label="Sans Serif"><span class="theme-font-seg__label">Sans Serif</span></button>
|
||
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="mono" aria-label="Mono"><span class="theme-font-seg__label">Mono</span></button>
|
||
</div>
|
||
</section>
|
||
<section class="theme-picker__section" aria-labelledby="theme-mesh-bg-title">
|
||
<h3 id="theme-mesh-bg-title" class="theme-picker__section-title">Page background</h3>
|
||
<div class="radius-seg radius-seg--two" role="radiogroup" aria-labelledby="theme-mesh-bg-title">
|
||
<button type="button" class="radius-seg__btn theme-mesh-bg-seg__btn" role="radio" aria-checked="false" data-mesh-bg-option="on" aria-label="Show background gradient"><span class="theme-mesh-bg-seg__label">On</span></button>
|
||
<button type="button" class="radius-seg__btn theme-mesh-bg-seg__btn" role="radio" aria-checked="false" data-mesh-bg-option="off" aria-label="Hide background gradient"><span class="theme-mesh-bg-seg__label">Off</span></button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="fab fab--primary" href="#contact" aria-label="Contact">
|
||
<span class="fab-primary__ico" aria-hidden="true"></span>
|
||
</a>
|
||
</div>
|
||
<div class="content-edit-fab-stack" aria-label="Content Edits" data-content-edit-ui>
|
||
<button
|
||
type="button"
|
||
class="fab fab--secondary glass-neutral content-edit-fab"
|
||
data-content-edit-mode-toggle
|
||
hidden
|
||
aria-pressed="false"
|
||
aria-label="Content Editing Mode"
|
||
title="Edit Mode"
|
||
>
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||
<path
|
||
d="M4 20h4l10.5-10.5a2.12 2.12 0 0 0 0-3L16.5 4.5a2.12 2.12 0 0 0-3 0L3 15v5z"
|
||
stroke="currentColor"
|
||
stroke-width="1.75"
|
||
stroke-linejoin="round"
|
||
/>
|
||
</svg>
|
||
<span class="content-edit-fab__badge" hidden aria-hidden="true">0</span>
|
||
</button>
|
||
</div>
|
||
|
||
|
||
<main id="case-main" class="section section--tight">
|
||
<div class="section__inner case-article" data-case-sequential-images>
|
||
<p class="hero__eyebrow">Figma · Plugin</p>
|
||
<h1 class="case-page__h1">AI Assistant</h1>
|
||
<p class="case-page__sub">A bridge between Cursor IDE and Figma for creating and running scripts</p>
|
||
|
||
<p class="section__prose">
|
||
I worked in Figma every day, designing software, apps and websites. I used plenty of plugins and made a few of my own when I could not find what I needed. The same manual steps kept coming back. Organizing layers was tedious, and some tasks felt unrealistic to do by hand, so I kept putting them off.
|
||
</p>
|
||
|
||
<h2 id="context">Context</h2>
|
||
<p class="section__prose">
|
||
I built the plugin for myself. I was already using LLMs for different tasks and asking them to help write Figma scripts. I created the code in Cursor, then had to move it into Figma manually to run it. I did not know how to write plugin or script code myself, so I learned from Figma's documentation and plugin API as I went, while refining my script-generation rules in Cursor.
|
||
</p>
|
||
|
||
<h2 id="task">Task</h2>
|
||
<p class="section__prose">
|
||
I wanted to remove the manual code handoff and run scripts directly in Figma. The plugin had to connect Cursor and Figma, let me reuse existing scripts, and make it easy to copy context about a selected node or an entire page for the LLM.
|
||
</p>
|
||
<aside class="case-callout" data-callout>
|
||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">💡</span>
|
||
<div class="case-callout__content">
|
||
<p>I could add scripts for different tasks to the catalog and run them directly in Figma.</p>
|
||
</div>
|
||
</aside>
|
||
<figure class="case-media case-media--real">
|
||
<img
|
||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||
alt="Catalog of custom plugin scripts"
|
||
data-src="../../../assets/ai-assistant-figma/Scripts%20List.webp"
|
||
loading="lazy"
|
||
decoding="async"
|
||
data-image-viewer
|
||
/>
|
||
<figcaption>Script catalog with search, sorting, and quick launch.</figcaption>
|
||
</figure>
|
||
|
||
<h2 id="solution">Solution</h2>
|
||
<p class="section__prose">
|
||
A local server and WebSocket connected Cursor to the plugin. Once a script was ready, I opened the plugin and clicked Run. The code was sent to Figma automatically.
|
||
</p>
|
||
<figure class="case-media case-media--real">
|
||
<img
|
||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||
alt="Script editor in the Figma plugin"
|
||
data-src="../../../assets/ai-assistant-figma/Script%20Edit.webp"
|
||
loading="lazy"
|
||
decoding="async"
|
||
data-image-viewer
|
||
/>
|
||
<figcaption>Script screen before running it in Figma.</figcaption>
|
||
</figure>
|
||
<p class="section__prose">
|
||
Saved scripts could be launched from the catalog without Cursor. I ran existing scripts more often than I created new ones.
|
||
</p>
|
||
<figure class="case-media case-media--real">
|
||
<img
|
||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||
alt="Menu of additional script actions"
|
||
data-src="../../../assets/ai-assistant-figma/Menu%20More.webp"
|
||
loading="lazy"
|
||
decoding="async"
|
||
data-image-viewer
|
||
/>
|
||
<figcaption>Menu of additional actions for a script.</figcaption>
|
||
</figure>
|
||
<p class="section__prose">
|
||
To give the LLM better context, the plugin could copy information about a selected node or an entire page. I used that data in my conversation with the model.
|
||
</p>
|
||
<figure class="case-media case-media--real">
|
||
<img
|
||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||
alt="Plugin toolbar with the Live toggle"
|
||
data-src="../../../assets/ai-assistant-figma/Live%20Mode.webp"
|
||
loading="lazy"
|
||
decoding="async"
|
||
data-image-viewer
|
||
/>
|
||
<figcaption>Script controls with the Live toggle.</figcaption>
|
||
</figure>
|
||
<p class="section__prose">
|
||
The LLM in Cursor could read the execution logs. If the issue was with the script's logic rather than a bug, I split it into small steps and ran them one by one. I tested each script in a separate Figma file first, then tried it in a working file when it behaved as expected.
|
||
</p>
|
||
<figure class="case-media case-media--real">
|
||
<img
|
||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||
alt="Script editor with an execution log panel"
|
||
data-src="../../../assets/ai-assistant-figma/Script%20Edit%20Log%20View.webp"
|
||
loading="lazy"
|
||
decoding="async"
|
||
data-image-viewer
|
||
/>
|
||
<figcaption>Script editor and execution log panel.</figcaption>
|
||
</figure>
|
||
|
||
<figure class="case-media case-media--real">
|
||
<div class="case-video-shell">
|
||
<video controls preload="metadata" playsinline poster="../../../assets/ai-assistant-figma/video/20250904-1414-49.4157894.webp" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
|
||
<source src="../../../assets/ai-assistant-figma/video/20250904-1414-49.4157894.webm" type="video/webm" />
|
||
<source src="../../../assets/ai-assistant-figma/video/20250904-1414-49.4157894.mp4" type="video/mp4" />
|
||
Your browser does not support the video tag.
|
||
</video>
|
||
</div>
|
||
<figcaption>Editing a script in Cursor and checking the result in Figma.</figcaption>
|
||
</figure>
|
||
|
||
<h2 id="result">Result</h2>
|
||
<aside class="case-callout" data-callout>
|
||
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">✅</span>
|
||
<div class="case-callout__content">
|
||
<p>I estimated that a task could take 1 to 30 minutes by hand. Launching its script then took one click.</p>
|
||
</div>
|
||
</aside>
|
||
<figure class="case-media case-media--real">
|
||
<img
|
||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||
alt="Script execution indicator"
|
||
data-src="../../../assets/ai-assistant-figma/Script%20is%20Working.webp"
|
||
loading="lazy"
|
||
decoding="async"
|
||
data-image-viewer
|
||
/>
|
||
<figcaption>Indicator that a script is running.</figcaption>
|
||
</figure>
|
||
<p class="section__prose">
|
||
The time saved let me try more complex workflows. In working files, I often ran a script to flatten vector icons so they displayed correctly in production. Another turned a frame into a component, created an instance, and moved the master component to a designated place on the components page. Scripts also updated color tokens after manual changes and helped build a design-system file with many pages.
|
||
</p>
|
||
<p class="section__prose">
|
||
Other tasks included adding component variants, saving a set of tokens from JSON stored in a text layer, and building a prototype with keyboard-driven transitions. I also automated code syntax highlighting in a mockup to present an idea and tune the syntax colors. I would not have colored code by hand.
|
||
</p>
|
||
<figure class="case-media case-media--real">
|
||
<div class="case-video-shell">
|
||
<video controls preload="metadata" playsinline poster="../../../assets/ai-assistant-figma/video/20250913-0936-57.5977819.webp" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
|
||
<source src="../../../assets/ai-assistant-figma/video/20250913-0936-57.5977819.webm" type="video/webm" />
|
||
<source src="../../../assets/ai-assistant-figma/video/20250913-0936-57.5977819.mp4" type="video/mp4" />
|
||
Your browser does not support the video tag.
|
||
</video>
|
||
</div>
|
||
<figcaption>A script assembles a form from design-system components using a reference.</figcaption>
|
||
</figure>
|
||
<p class="section__prose">
|
||
I learned more about the Figma Plugin API. Now I plan work with scriptable actions in mind, so I spend less time on repetitive steps.
|
||
</p>
|
||
|
||
<h2 id="next-steps">Plans</h2>
|
||
<p class="section__prose">
|
||
Next, I want to remove Cursor from the workflow and talk to LLM agents directly in Figma. Depending on the task, an agent could create a reusable script or carry out an action from a text command.
|
||
</p>
|
||
<p class="section__prose">
|
||
For now, only the interface window exists. Agent conversations and actions are not implemented yet.
|
||
</p>
|
||
<figure class="case-media case-media--real">
|
||
<div class="case-video-shell">
|
||
<video controls preload="metadata" playsinline poster="../../../assets/ai-assistant-figma/video/20260916-132035.webp" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
|
||
<source src="../../../assets/ai-assistant-figma/video/20260916-132035.webm" type="video/webm" />
|
||
<source src="../../../assets/ai-assistant-figma/video/20260916-132035.mp4" type="video/mp4" />
|
||
Your browser does not support the video tag.
|
||
</video>
|
||
</div>
|
||
<figcaption>Interface concept for the planned LLM agent workflow in Figma.</figcaption>
|
||
</figure>
|
||
</div>
|
||
</main>
|
||
|
||
<section class="section section--surface" data-reveal>
|
||
<div class="section__inner">
|
||
<h2 class="section__title">See also</h2>
|
||
<ol class="mini-projects-grid" data-mini-projects-list>
|
||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="ai-assistant">
|
||
<a
|
||
class="mp-card mp-card--link mp-card--compact"
|
||
href="../ai-assistant-figma/"
|
||
aria-label="AI Assistant — Open a Case"
|
||
>
|
||
<div class="mp-card__body">
|
||
<h3 class="mp-card__title">AI Assistant</h3>
|
||
<ul class="mp-card__tags" aria-label="Project Type">
|
||
<li class="mp-card__tag">Figma plugin</li>
|
||
</ul>
|
||
<p class="mp-card__desc">A Figma plugin with AI features.</p>
|
||
</div>
|
||
</a>
|
||
</li>
|
||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="card-columns">
|
||
<a
|
||
class="mp-card mp-card--link mp-card--compact"
|
||
href="../card-columns-obsidian/"
|
||
aria-label="Card Columns — Open a Case"
|
||
>
|
||
<div class="mp-card__body">
|
||
<h3 class="mp-card__title">Card Columns</h3>
|
||
<ul class="mp-card__tags" aria-label="Project Type">
|
||
<li class="mp-card__tag">Obsidian Plugin</li>
|
||
</ul>
|
||
<p class="mp-card__desc">A plugin for displaying cards in columns in Obsidian.</p>
|
||
</div>
|
||
</a>
|
||
</li>
|
||
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="site-portfolio">
|
||
<a
|
||
class="mp-card mp-card--link mp-card--compact"
|
||
href="../site-portfolio/"
|
||
aria-label="This website — Open a Case"
|
||
>
|
||
<div class="mp-card__body">
|
||
<h3 class="mp-card__title">This website</h3>
|
||
<ul class="mp-card__tags" aria-label="Project Type">
|
||
<li class="mp-card__tag">Development with AI IDE</li>
|
||
</ul>
|
||
<p class="mp-card__desc">Website development using AI IDE.</p>
|
||
</div>
|
||
</a>
|
||
</li>
|
||
</ol>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="contact" class="section section--surface section--footerish" data-reveal>
|
||
<div class="section__inner">
|
||
<h2 class="section__title">Contact</h2>
|
||
<ul class="contact-grid">
|
||
<li><a class="contact-card contact-card--link contact-card--brand-tg" href="https://t.me/mamadaevv" rel="noopener noreferrer"><span class="contact-card__corner" aria-hidden="true"></span><span class="contact-card__icon" aria-hidden="true"><img src="../../../assets/shared/icons/tg.svg" alt="" width="24" height="24" loading="eager" decoding="async" /></span><span class="contact-card__body"><span class="contact-card__name">Telegram</span><span class="contact-card__hint">I'll reply today</span></span></a></li>
|
||
<li><a class="contact-card contact-card--link contact-card--brand-whatsapp" href="https://wa.me/qr/E2AWW5ZHE3ANJ1" rel="noopener noreferrer"><span class="contact-card__corner" aria-hidden="true"></span><span class="contact-card__icon" aria-hidden="true"><img src="../../../assets/shared/icons/whatsapp.svg" alt="" width="24" height="24" loading="eager" decoding="async" /></span><span class="contact-card__body"><span class="contact-card__name">WhatsApp</span><span class="contact-card__hint">Stable connection</span></span></a></li>
|
||
|
||
<li><button type="button" class="contact-card contact-card--action contact-card--brand-email" data-contact-copy data-email-b64="bmlrQG1hbWFkYWV2di5zaXRl" data-hint-default="Copy address" aria-label="Copy email address to clipboard"><span class="contact-card__corner contact-card__corner--copy" aria-hidden="true"></span><span class="contact-card__icon contact-card__icon--accent" aria-hidden="true"></span><span class="contact-card__body"><span class="contact-card__name">Email</span><span class="contact-card__hint" data-contact-copy-hint>Copy address</span></span></button></li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<footer class="footer">
|
||
<div class="footer__inner">
|
||
<span class="footer__chunk footer__chunk--date">© 2015–2026<span class="footer__sep" aria-hidden="true"> ·
|
||
</span></span>
|
||
<span class="footer__chunk footer__chunk--main">Website design and development — Nikolay Mamadaev</span> ·<span class="footer__chunk footer__chunk--meta footer__attr"><span class="footer__sep" aria-hidden="true"> </span><a href="../../cases/design-system/">Design System</a><span class="footer__sep" aria-hidden="true">·
|
||
</span><a href="../../cookie-policy/">Cookie policy</a></span> ·<span class="footer__chunk footer__chunk--lang"><span class="footer__sep" aria-hidden="true">
|
||
</span><nav class="lang-switch" aria-label="Choose language" data-lang-switch>
|
||
<a class="lang-switch__btn" data-lang="ru" href="/">Русский</a>
|
||
</nav>
|
||
<script>
|
||
(function () {
|
||
var p = location.pathname.replace(/^\/+/, "");
|
||
var isEn = p.indexOf("en/") === 0;
|
||
var base = isEn ? p.slice(3) : p;
|
||
var links = document.querySelectorAll("[data-lang-switch] [data-lang]");
|
||
for (var i = 0; i < links.length; i++) {
|
||
var l = links[i].getAttribute("data-lang");
|
||
links[i].setAttribute("href", (l === "en" ? "/en/" : "/") + base);
|
||
}
|
||
})();
|
||
</script>
|
||
</span></div>
|
||
</footer>
|
||
|
||
<aside class="cookie-notice glass-neutral" hidden data-cookie-notice>
|
||
<p class="cookie-notice__text">
|
||
This site uses cookies to ensure a smooth user experience. For more information, see the
|
||
<a href="../../cookie-policy/" data-cookie-policy-link>cookie policy</a>.
|
||
</p>
|
||
<button type="button" class="btn btn--secondary btn--flat cookie-notice__accept" data-cookie-accept>
|
||
<span>Accept</span>
|
||
|
||
<span class="cookie-notice__accept-icon" aria-hidden="true"><img src="../../../assets/shared/icons/сookie.svg" alt="" width="20" height="20" loading="eager" decoding="async" /></span>
|
||
|
||
</button>
|
||
</aside>
|
||
|
||
<div id="nav-more-menu-floating" class="nav-more__floating" hidden data-nav-more-menu-floating aria-hidden="true"></div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded',function(){
|
||
var s=document.body.dataset.currentCase;
|
||
if(!s)return;
|
||
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|