UX Hero Воркбук до відео · Автоматизація Figma 0 / 7
Figma · Code Runner · VooDoo Maker · 2 безкоштовні плагіни

Автоматизація Figma: як зекономити 80% часу з плагінами UX Hero

Сотні хексів замість змінних і девелопер, який пише свій синій. Code Runner одним скриптом розбирає всі кольори, будує з них токени і прив'язує кожен шар. Плюс звіт по файлу, кейси, що ще вміє Code Runner, і VooDoo Maker для системи з нуля.

Встановити Code Runner VooDoo Maker ↗
Audit → хекси в токени → звіт → кейси → VooDooпрацює на безкоштовній Figma0 грн
00
проблема · 1 хв

Коли токени розлетілися

Пара ітерацій з клієнтом, кілька правок «на швидку» - і в макеті сотні хексів замість змінних. Девелопер бачить #2E6BFF і пише свій синій. Дизайн-система ніби є, а в коді її немає.

Що зробимо: Code Runner покаже, скільки шарів живе на хексах. Один скрипт розбере всі кольори, сам збудує з них токени і прив'яже кожен шар. Далі - розбір файлу, кейси, що ще вміє Code Runner, і VooDoo Maker, який збирає болванку дизайн-системи з нуля.

01
один раз · 1 хв

Встанови два плагіни

Обидва безкоштовні і працюють на безкоштовній Figma.

  • UX Hero: Code Runner - запускає JavaScript прямо у файлі. Скрипти пише Claude. Figma Community ↗
  • UX Hero: VooDoo Maker - дизайн-система з одного кольору: змінні в 3 шарах, світла і темна тема, текстові стилі, компоненти. Figma Community ↗

У Community тисни Open in… → свій файл. Далі плагін є в правому кліку: Plugins → UX Hero: …

02
Code Runner · 1 хв

Порахуй, скільки шарів на хексах

  1. Правий клік → Plugins → UX Hero: Code Runner.
  2. У рядку Snippets натисни Audit colors, потім Run.
  3. Плагін виділить усі шари з хексами, а в консолі покаже таблицю: шар і його кольори. Внизу - скільки таких шарів. Запам'ятай цифру: в кінці порівняємо.
03
Code Runner · 2 хв

Хекси → токени одним скриптом

Скрипт збирає всі кольори без змінних, склеює майже однакові (#2E6BFF і #2F6CFE) і будує систему у 2 шари:

  • Brand · Primitives - сама палітра: blue/400, neutral/950, white. Сховані з пікера кольорів, щоб у макеті їх не брали напряму.
  • Brand · Theme - ролі, що посилаються на палітру: text/primary, bg/canvas, border/default, action/primary. Роль визначає тип шару: текст, рамка, заливка.

Потім прив'язує кожен шар до його ролі. Кольори, які вже є змінними у файлі, не дублює.

  1. Скопіюй скрипт, встав у Code Runner, Run. Хочеш спершу подивитись план - постав зверху APPLY = false: скрипт лише покаже таблицю.
  2. Відкрий Variables: дві колекції. Перейменуй будь-що - шари лишаються прив'язаними.
  3. Ще раз Audit colors: «No hardcoded colors». Шари всередині інстансів виправляються в головному компоненті.
скрипт для Code Runner · хекси → токени
// UX Hero · Code Runner: hardcoded hex -> design tokens
// Reads every raw color on the page, groups near-identical ones, and builds a 2-layer system:
//   Brand · Primitives  - the palette itself: blue/500, neutral/100, white ... (hidden from pickers)
//   Brand · Theme       - roles that point to primitives: text/primary, bg/canvas, border/default, action/primary
// Then binds every layer to its role token. Text gets text/..., strokes get border/..., fills get bg/... or action/...
// Colors that already exist as a variable in the file are reused, not duplicated.
// Scope: selection, or the whole page if nothing is selected. Layers inside instances are skipped
// (fix them in the main component). Undo: one Cmd+Z. Safe to run again.

const APPLY = true;   // false = only show the plan in the console, change nothing
const MERGE = 6;      // 0-255: colors this close become one token (#2E6BFF and #2F6CFE)

const roots = figma.currentPage.selection.length ? [...figma.currentPage.selection] : [figma.currentPage];
const toHex = c => '#' + [c.r, c.g, c.b].map(v => Math.round(v * 255).toString(16).padStart(2, '0')).join('').toUpperCase();
const dist = (a, b) => Math.max(Math.abs(a.r - b.r), Math.abs(a.g - b.g), Math.abs(a.b - b.b)) * 255;
function hsl(c) {
  const max = Math.max(c.r, c.g, c.b), min = Math.min(c.r, c.g, c.b), l = (max + min) / 2, d = max - min;
  if (!d) return { h: 0, s: 0, l };
  const s = d / (1 - Math.abs(2 * l - 1));
  let h = max === c.r ? ((c.g - c.b) / d) % 6 : max === c.g ? (c.b - c.r) / d + 2 : (c.r - c.g) / d + 4;
  return { h: (h * 60 + 360) % 360, s, l };
}
function family(c) {
  const { h, s, l } = hsl(c);
  if (l > 0.985) return 'white';
  if (l < 0.03) return 'black';
  if (s < 0.12 || Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b) < 0.07) return 'neutral'; // warm/cool greys stay neutral
  return h < 15 || h >= 345 ? 'red' : h < 40 ? 'orange' : h < 65 ? 'yellow' : h < 170 ? 'green'
    : h < 195 ? 'teal' : h < 250 ? 'blue' : h < 290 ? 'purple' : 'pink';
}
const step = c => Math.min(950, Math.max(50, Math.round((1 - hsl(c).l) * 20) * 50));

// 1. Raw colors on the page
const uses = [];
let inInstances = 0, withStyle = 0;
function walk(n, inInst) {
  for (const prop of ['fills', 'strokes']) {
    if (!(prop in n)) continue;
    const paints = n[prop];
    if (paints === figma.mixed || !Array.isArray(paints)) continue;
    const styleId = prop === 'fills' ? n.fillStyleId : n.strokeStyleId;
    paints.forEach((p, i) => {
      if (p.type !== 'SOLID' || p.visible === false || (p.boundVariables && p.boundVariables.color)) return;
      if (inInst) { inInstances++; return; }
      if (styleId && styleId !== figma.mixed) { withStyle++; return; }
      const role = n.type === 'TEXT' && prop === 'fills' ? 'text' : prop === 'strokes' ? 'border' : 'bg';
      uses.push({ node: n, prop, i, color: p.color, role });
    });
  }
  if ('children' in n) for (const ch of n.children) walk(ch, inInst || n.type === 'INSTANCE');
}
for (const r of roots) { if (r.type === 'PAGE') { for (const ch of r.children) walk(ch, false); } else walk(r, false); }
if (!uses.length) {
  console.log('Raw colors: 0. Skipped inside instances: ' + inInstances + ', with color styles: ' + withStyle);
  ok('Nothing to fix: every color is already a token');
  return;
}

// 2. Palette: merge near-identical colors, the most used one names the group
const palette = []; // { color, hex, count, uses[] }
const byHex = {};
for (const u of uses) { const h = toHex(u.color); (byHex[h] = byHex[h] || { color: u.color, hex: h, uses: [] }).uses.push(u); }
for (const g of Object.values(byHex).sort((a, b) => b.uses.length - a.uses.length)) {
  const near = palette.find(p => dist(p.color, g.color) <= MERGE);
  if (near) near.uses.push(...g.uses); else palette.push(g);
}

// 3. Primitive name per palette color: family/step (blue/500), white, black; collisions get -2, -3
const taken = {};
for (const p of palette) {
  const f = family(p.color);
  let name = f === 'white' || f === 'black' ? f : f + '/' + step(p.color);
  let k = name, n = 2;
  while (taken[k]) k = name + '-' + n++;
  taken[k] = 1; p.prim = k;
}

// 4. Role tokens: per role, ordered by usage. Neutral and saturated colors get different names.
const NAMES = {
  text:   { neutral: ['primary', 'secondary', 'muted', 'disabled'], accent: ['accent', 'link', 'accent-2'], light: ['inverse'] },
  bg:     { neutral: ['canvas', 'surface', 'subtle', 'muted'], accent: ['primary', 'secondary', 'tertiary'], dark: ['inverse'] },
  border: { neutral: ['default', 'subtle', 'strong'], accent: ['accent', 'focus'] },
};
const roles = {}; // 'text|blue/500' -> { role, prim, color, uses[] }
for (const p of palette) for (const u of p.uses) {
  const key = u.role + '|' + p.prim;
  (roles[key] = roles[key] || { role: u.role, p, uses: [] }).uses.push(u);
}
const used = {};
const roleList = Object.values(roles).sort((a, b) => b.uses.length - a.uses.length);
for (const r of roleList) {
  const { l } = hsl(r.p.color), accent = !/^(neutral|white|black)/.test(r.p.prim);
  const kind = accent ? 'accent' : r.role === 'text' && l > 0.85 ? 'light' : r.role === 'bg' && l < 0.2 ? 'dark' : 'neutral';
  const list = NAMES[r.role][kind] || NAMES[r.role].neutral;
  const group = r.role === 'bg' && kind === 'accent' ? 'action' : r.role;
  const idx = (used[group + kind] = (used[group + kind] || 0) + 1) - 1;
  r.name = group + '/' + (list[idx] || list[list.length - 1] + '-' + (idx - list.length + 2));
}

// 5. Report (and stop here if APPLY is false)
console.table(roleList.map(r => ({ token: r.name, primitive: r.p.prim, hex: r.p.hex, layers: r.uses.length })));
console.log(palette.length + ' colors -> ' + palette.length + ' primitives, ' + roleList.length + ' role tokens. ' +
  'Skipped: ' + inInstances + ' inside instances, ' + withStyle + ' with color styles.');
if (!APPLY) { ok('Plan only: set APPLY = true to create and bind'); return; }

// 6. Collections and variables (reused by name if they already exist)
const cols = await figma.variables.getLocalVariableCollectionsAsync();
const colP = cols.find(c => c.name === 'Brand · Primitives') || figma.variables.createVariableCollection('Brand · Primitives');
const colT = cols.find(c => c.name === 'Brand · Theme') || figma.variables.createVariableCollection('Brand · Theme');
const all = await figma.variables.getLocalVariablesAsync('COLOR');
const find = (col, name) => all.find(v => v.variableCollectionId === col.id && v.name === name);
const SCOPES = { text: ['TEXT_FILL'], border: ['STROKE_COLOR'], bg: ['FRAME_FILL', 'SHAPE_FILL'], action: ['FRAME_FILL', 'SHAPE_FILL'] };
let created = 0, bound = 0;
for (const p of palette) {
  let v = find(colP, p.prim);
  if (!v) { v = figma.variables.createVariable(p.prim, colP, 'COLOR'); if (!all.includes(v)) all.push(v); created++; }
  v.setValueForMode(colP.defaultModeId, { r: p.color.r, g: p.color.g, b: p.color.b, a: 1 });
  try { v.scopes = []; } catch (e) {} // primitives: hidden from the color picker, use roles instead
  p.v = v;
}
for (const r of roleList) {
  let v = find(colT, r.name);
  if (!v) { v = figma.variables.createVariable(r.name, colT, 'COLOR'); if (!all.includes(v)) all.push(v); created++; }
  v.setValueForMode(colT.defaultModeId, figma.variables.createVariableAlias(r.p.v));
  try { v.scopes = SCOPES[r.name.split('/')[0]]; } catch (e) {}
  for (const u of r.uses) {
    const paints = u.node[u.prop].slice();
    paints[u.i] = figma.variables.setBoundVariableForPaint(paints[u.i], 'color', v);
    u.node[u.prop] = paints;
    bound++;
  }
}
console.log('Rename freely in Variables: layers stay bound. Run Audit colors again to check.');
ok(palette.length + ' primitives, ' + roleList.length + ' role tokens, ' + bound + ' paints bound (' + created + ' new variables)');
04
Code Runner · 1 хв

Розбери файл: що в ньому насправді

Скрипт нічого не змінює, тільки рахує: шрифти і розміри, тексти без стилів, відступи не по сітці 4 px, радіуси, скільки кольорів на токенах, які компоненти використовуються і які фрейми схожі на відірвані від компонентів копії.

скрипт для Code Runner · звіт по файлу
// UX Hero · Code Runner: what is in this file
// Read-only report: fonts and sizes, spacing and radii, colors on tokens vs raw hex,
// components and detached copies. Changes nothing. Scope: selection or the whole page.

const roots = figma.currentPage.selection.length ? [...figma.currentPage.selection] : [...figma.currentPage.children];
const count = (map, key) => { map[key] = (map[key] || 0) + 1; };
const top = (map, n = 12) => Object.entries(map).sort((a, b) => b[1] - a[1]).slice(0, n);
const fonts = {}, sizes = {}, gaps = {}, pads = {}, radii = {}, comps = {};
let layers = 0, texts = 0, textNoStyle = 0, paintsBound = 0, paintsRaw = 0, frames = 0, autoLayout = 0, instances = 0, detached = 0;

function walk(n) {
  layers++;
  if (n.type === 'TEXT') {
    texts++;
    if (!n.textStyleId) textNoStyle++;
    if (n.fontName !== figma.mixed) count(fonts, n.fontName.family + ' ' + n.fontName.style);
    if (n.fontSize !== figma.mixed) count(sizes, n.fontSize + ' px');
  }
  for (const prop of ['fills', 'strokes']) {
    if (!(prop in n) || !Array.isArray(n[prop])) continue;
    for (const p of n[prop]) if (p.type === 'SOLID' && p.visible !== false) (p.boundVariables && p.boundVariables.color) ? paintsBound++ : paintsRaw++;
  }
  if (n.type === 'FRAME' || n.type === 'COMPONENT') {
    frames++;
    if (n.layoutMode && n.layoutMode !== 'NONE') {
      autoLayout++;
      count(gaps, n.itemSpacing);
      [n.paddingTop, n.paddingRight, n.paddingBottom, n.paddingLeft].forEach(v => count(pads, v));
    }
    if (n.type === 'FRAME' && /^(button|btn|input|card|badge|chip|tag)/i.test(n.name)) detached++;
  }
  if ('cornerRadius' in n && n.cornerRadius !== figma.mixed && n.cornerRadius > 0) count(radii, n.cornerRadius);
  if (n.type === 'INSTANCE') { instances++; count(comps, n.name.split(/[\/,=]/)[0].trim()); return; }
  if ('children' in n) n.children.forEach(walk);
}
roots.forEach(walk);

const off4 = obj => Object.keys(obj).map(Number).filter(v => v % 4 !== 0);
console.log('Layers ' + layers + ' · frames ' + frames + ' (' + autoLayout + ' with auto layout) · instances ' + instances);
console.log('Colors: ' + paintsBound + ' on tokens, ' + paintsRaw + ' raw hex (' + Math.round(paintsBound / Math.max(1, paintsBound + paintsRaw) * 100) + '% tokenized)');
console.log('Text: ' + texts + ' layers, ' + textNoStyle + ' without a text style');
console.table(top(fonts).map(([font, n]) => ({ font, layers: n })));
console.table(top(sizes).map(([size, n]) => ({ size, layers: n })));
console.table(top(gaps).map(([gap, n]) => ({ gap, frames: n, grid4: gap % 4 === 0 ? 'ok' : 'off' })));
console.log('Paddings off the 4 px grid: ' + (off4(pads).join(', ') || 'none') + ' · radii used: ' + Object.keys(radii).join(', '));
console.table(top(comps).map(([component, n]) => ({ component, instances: n })));
if (detached) console.warn(detached + ' frames are named like components (button, card...) but are not instances: likely detached');
ok(layers + ' layers analysed: ' + Math.round(paintsBound / Math.max(1, paintsBound + paintsRaw) * 100) + '% colors on tokens, ' + Object.keys(sizes).length + ' font sizes');

Звіт - готовий список задач. Кожен рядок можна віддати Claude: «ось звіт, напиши скрипт, який виправить відступи 13 і 15 на 12 і 16».

05
Code Runner + Claude · кейси

Що ще можна зробити: екрани, компоненти, сторінки

Готові сніпети в самому плагіні - один клік, без Claude: Button, Input, Card, Badge, Avatar, Toast, Palette, Audit colors. Кожен створює компонент з усіма станами в auto layout.

Свої задачі - через кнопку Copy prompt for Claude: вона копіює інструкцію, як писати скрипти саме для Code Runner. Встав її в Claude, допиши задачу своїми словами - отримаєш код, який одразу запускається. Готові задачі:

задача для Claude · екрани · логін і головна на твоїх токенах
Напиши скрипт для Code Runner (Figma Plugin API, async).
Створи на поточній сторінці 2 екрани 390×844 поруч:
- «Вхід»: лого, поля Email і Пароль, кнопка «Увійти», лінк «Забув пароль».
- «Головна»: привітання, 3 картки зі статистикою, список з 5 операцій.
Усе в auto layout, відступи кратні 8, тексти українською.
Кольори бери зі змінних колекції «Brand · Theme» (bg/canvas, text/primary,
action/primary...) і прив'язуй через setBoundVariableForPaint.
Якщо такої колекції немає - використай DS з хелперів.
задача для Claude · компонент · Input з 4 станами
Напиши скрипт для Code Runner (Figma Plugin API, async).
Створи component set Input: стани Default, Focus, Error, Disabled.
Властивості компонента: Label (текст) і Show hint (boolean).
Висота 44, радіус 8, auto layout. Кольори - зі змінних «Brand · Theme».
задача для Claude · сторінка · вайрфрейм лендингу 1440
Напиши скрипт для Code Runner (Figma Plugin API, async).
Створи вайрфрейм лендингу 1440 px: hero з заголовком і кнопкою,
3 картки переваг, 3 тарифи, FAQ з 4 питань, футер.
Картинки - сірі прямокутники з підписом, що там має бути.
Усе в auto layout, сітка 12 колонок, тексти українською.
задача для Claude · дані · заповнити таблицю 20 рядками
Напиши скрипт для Code Runner (Figma Plugin API, async).
На виділеному фреймі є список або таблиця. Перший рядок - шаблон.
Продублюй його до 20 рядків і заповни правдоподібними даними українською:
ім'я, email, роль, статус (Активний / Запрошено / Вимкнено).
задача для Claude · перевірка · контраст WCAG по всій сторінці
Напиши скрипт для Code Runner (Figma Plugin API, async).
Перевір контраст кожного тексту на сторінці з фоном під ним
(WCAG AA: 4.5:1 для тексту до 18 px, 3:1 для більшого).
Виведи в консоль таблицю порушень: шар, колір, фон, контраст.
Виділи ці шари на канвасі.
задача для Claude · порядок · auto layout усім фреймам
Напиши скрипт для Code Runner (Figma Plugin API, async).
Усім фреймам на виділенні без auto layout увімкни вертикальний auto layout:
gap 16, padding 24, порядок дочірніх шарів - за координатою y.
Шари всередині інстансів не чіпай. Виведи, скільки фреймів змінив.
задача для Claude · порядок · перейменувати шари за змістом
Напиши скрипт для Code Runner (Figma Plugin API, async).
Перейменуй шари на виділеному фреймі за змістом.
- Тексти: перші 3-4 слова тексту.
- Фрейми з одним текстом і заливкою: button/<текст>.
- Картинки: image/<назва батьківського блоку>.
Решту не чіпай. У консоль виведи таблицю «було → стало».
задача для Claude · типографіка · 13 px → 14 px
Напиши скрипт для Code Runner (Figma Plugin API, async).
На поточній сторінці знайди всі тексти розміром 13 px
і зроби їх 14 px з line-height 20 px. Шрифт перед зміною завантаж.
Тексти всередині інстансів не чіпай. Виведи, скільки змінив.

Якщо скрипт впав - скопіюй помилку з консолі назад у Claude: «ось помилка, виправ». Зазвичай другий запуск уже працює.

06
VooDoo Maker · 2 хв

Болванка дизайн-системи з нуля

Коли файлу ще немає - не треба нічого розбирати. Відкрий порожній файл і збери систему з одного кольору.

  1. Новий порожній файл → правий клік → Plugins → UX Hero: VooDoo Maker.
  2. Обери пресет або впиши свій Primary. Secondary, Error, Success, Warning - за бажанням, нейтральні VooDoo виведе сам.
  3. Справа обери Dark або Light - так виглядатиме превʼю і тема за замовчуванням.
  4. Generate design system. За хвилину: 150+ змінних у 3 колекціях (Primitives → Theme → Components), текстові стилі, 12 компонентів зі станами і сторінка-обкладинка.

На безкоштовній Figma дозволений лише 1 режим змінних, тож VooDoo будує ту тему, яку ти обрав. На платному плані будуть обидві: Light і Dark.

07
бонус · Claude · 2 хв

Сайт з посилання: вайрфрейм, потім картинки

Як я починаю редизайн: даю Claude посилання на поточний сайт і прошу спершу структуру, а вже потім картинки. Так видно, що на сторінці зайве, ще до того, як з'явився стиль.

крок 1 · вайрфрейм з посилання
Ось сайт: [посилання].
Перемалюй головну як вайрфрейм: ті самі блоки в тому ж порядку,
справжні тексти з сайту, сітка 12 колонок, ширина 1440.
Картинки - сірі плейсхолдери з підписом, що там має бути.
Без кольорів і стилю, тільки структура та ієрархія.
крок 2 · картинки
Тепер постав картинки: у кожен плейсхолдер - зображення
під зміст свого блоку. Сітку, тексти і розміри не чіпай.
Покажи, що вийшло, і список блоків, де картинку варто замінити на справжню.

Картинки від AI - для презентації ідеї. У фінальний макет став справжні фото клієнта.

Що далі

VooDoo Maker + Code Runner

Обидва безкоштовні і працюють на безкоштовній Figma. Нові скрипти для Code Runner і промпти - у Telegram-боті, разом з анонсами ефірів.

VooDoo Maker ↗   Code Runner ↗
Підписатися в боті ↗
Курс «Дизайн-система + AI»

Як побудувати систему, яка не розлітається: Variables, 3 рівні токенів, компоненти зі станами, Storybook і Claude. 7 днів, воркбук, відео і група.

Про курс ↗
Щочетверга ефір

О 19:00 роблю щось руками на екрані, без слайдів. Реєстрація і нагадування в боті.

Усі ефіри ↗