Рецепты Plugin API (references/build-recipes.md)
Источник:
ds-skills/src/skills/astra-figma-doc-build/references/build-recipes.md— справочник скилла astra-figma-doc-build
Код ниже — чистый Plugin API: выполняется через use_figma или любой другой инструмент,
который запускает JS в файле Figma. Контракт вызова — в SKILL.md, раздел «Как запускать».
Код написан без ?., ?? и structuredClone: песочница Figma MCP их не знает.
Значения в ВЕРХНЕМ_РЕГИСТРЕ — подстановки: id и числа из ответа прошлого вызова,
переданные строкой. Состояние между вызовами не живёт.
0. Общие функции
Вставляются в начало каждого вызова, который пишет.
// свойство по имени до «#» — точно, с проверкой типа:
// у ds-log есть «Описание» (BOOLEAN) и «Описание » (TEXT) — отличаются пробелом
function propKey(inst, name, type) {
const props = inst.componentProperties;
const hits = Object.keys(props).filter(k =>
k.split("#")[0] === name && (!type || props[k].type === type));
if (hits.length !== 1) {
throw new Error("свойство «" + name + "» (" + (type || "любой тип") + "): найдено " +
hits.length + " из " + Object.keys(props).join(", "));
}
return hits[0];
}
// map: { имя: значение }; тип выводится из значения: boolean → BOOLEAN,
// строка → TEXT или VARIANT (что найдётся)
function setProps(inst, map) {
const out = {};
for (const name of Object.keys(map)) {
const v = map[name];
let key;
if (typeof v === "boolean") key = propKey(inst, name, "BOOLEAN");
else {
try { key = propKey(inst, name, "TEXT"); }
catch (e) { key = propKey(inst, name, "VARIANT"); }
}
out[key] = v;
}
inst.setProperties(out);
}
function variantOf(set, props) {
const hit = set.children.find(c =>
Object.keys(props).every(k => c.variantProperties[k] === props[k]));
if (!hit) throw new Error(set.name + ": нет варианта " + JSON.stringify(props));
return hit;
}
// неразрывные пробелы — texts.md, раздел 4
const NBSP = String.fromCharCode(160); // \s в регулярках его и так ловит
function nb(s) {
let t = String(s);
for (let i = 0; i < 2; i++) {
t = t.replace(/(^|[\s(«])([а-яёА-ЯЁ]{1,2}|для|без|при|под|над|про|или|как) /g, "$1$2" + NBSP);
}
return t.replace(/ (—|–)/g, NBSP + "$1").replace(/(\d) (?=[^\s\d—–])/g, "$1" + NBSP);
}
1. Инвентарь компонента (только чтение)
Проверка входа — здесь, до любой записи.
const node = await figma.getNodeByIdAsync("SRC_ID");
if (!node) return { stop: true, reason: "узел не найден" };
if (node.type !== "COMPONENT_SET" && node.type !== "COMPONENT") {
return { stop: true, got: { type: node.type, name: node.name },
reason: "нужна ссылка на сам компонент: COMPONENT_SET или COMPONENT" };
}
const set = node.type === "COMPONENT" && node.parent && node.parent.type === "COMPONENT_SET"
? node.parent : node;
let page = set; while (page.type !== "PAGE") page = page.parent;
let section = set.parent;
while (section && section.type !== "SECTION" && section.type !== "PAGE") section = section.parent;
const oldDocs = section && section.type === "SECTION"
? { id: section.id, name: section.name, frames: section.children.map(c => c.name) } : null;
const defs = set.componentPropertyDefinitions;
const props = Object.keys(defs).map(k => ({
key: k, name: k.split("#")[0], type: defs[k].type,
values: defs[k].variantOptions, def: defs[k].defaultValue
}));
const sample = set.type === "COMPONENT_SET" ? set.defaultVariant : set;
function tree(n, d) {
const o = { id: n.id, name: n.name, type: n.type };
if (n.layoutMode && n.layoutMode !== "NONE") o.layout = n.layoutMode;
if (n.componentPropertyReferences) o.refs = n.componentPropertyReferences;
if (n.type === "INSTANCE") o.main = n.mainComponent ? n.mainComponent.name : null;
if ("children" in n && d < 3 && n.type !== "INSTANCE") o.kids = n.children.map(c => tree(c, d + 1));
return o;
}
return {
page: { id: page.id, name: page.name }, oldDocs,
set: { id: set.id, type: set.type, name: set.name, key: set.key,
description: set.description, docs: set.documentationLinks,
w: Math.round(set.width), h: Math.round(set.height) },
props,
defaultVariant: sample.id,
variants: set.type === "COMPONENT_SET"
? set.children.slice(0, 300).map(c => ({ id: c.id, p: c.variantProperties,
x: Math.round(c.x), y: Math.round(c.y), w: Math.round(c.width), h: Math.round(c.height) }))
: [],
anatomy: tree(sample, 0)
};
Больше 300 вариантов или ответ упёрся в 20 КБ — снимать варианты отдельным вызовом
кусками по slice. Токены мастера по умолчанию — отдельно, get_variable_defs.
Для BOOLEAN найти слой, который он прячет: узел дерева с refs.visible = ключ свойства.
Имя этого слоя пойдёт в описание блока (texts.md).
2. Импорт атомов и шрифты
Каждый вызов, который пишет, начинает с импорта нужного и загрузки шрифтов.
const K = {
template: "8f7cbc56b451e2dafa03085a38c7b28835d397fa", // ds-component, только проверка
pageLog: "cc0b93384365461d9de59ebef15fbbe2f7c7ee4c", // История изменений
pageSpec: "54dba64bb1ed6734946f020991c07b30459cc21f", // Спецификация
pageComp: "cb2cff7d2706d72096e1e206734e05e275771489", // ❖ Компонент
devMode: "a9e5a749b4add3badd7752e7c068489722c5d2be", // ds-paragraph-dev-mode
head: "5d877a1052d6fd833a5969feac53fb80621aece8", // ds-format-component-head
footer: "72910704eec91328387532f35dc2640b7b6e9acf" // ds-format-component-footer
};
const KS = {
log: "00d9e32bc28904a27517489b84f9f281a39b2011", // ds-log
para: "ca2f04a5efaa20c9d9ae32a396330b7a13e696e9", // ds-paragraph
mc: "f9ee550e7ecc537fc63075015db4c76d0355a619", // ds-mcp-component
state: "ddafef2738b4d3183aa5e0e418da8fa23ede1cf2", // ds-mcp-component-state
label: "9d9fc72e383e614ac585a65acdeb7a621e094875" // ds-mcp-component-label
};
const C = {}, S = {}, missing = [];
for (const n of Object.keys(K)) {
try { C[n] = await figma.importComponentByKeyAsync(K[n]); } catch (e) { missing.push(n); }
}
for (const n of Object.keys(KS)) {
try { S[n] = await figma.importComponentSetByKeyAsync(KS[n]); } catch (e) { missing.push(n); }
}
if (missing.length) return { stop: true, missing,
reason: "не импортируется — включите «Astra DS: Docs» в Assets → Libraries или обновите engine-map.md" };
// шрифты — с текстов страниц и атомов, не списком
const fonts = new Map();
for (const c of [C.pageLog, C.pageSpec, C.pageComp, S.state.defaultVariant, S.label.defaultVariant]) {
for (const t of c.findAllWithCriteria({ types: ["TEXT"] })) {
for (const seg of t.getStyledTextSegments(["fontName"])) {
fonts.set(seg.fontName.family + "|" + seg.fontName.style, seg.fontName);
}
}
}
await Promise.all([...fonts.values()].map(f => figma.loadFontAsync(f)));
В шаге 0 вызов возвращает { ok: true, fonts: [...fonts.keys()] }. В остальных вызовах
этот блок просто предваряет работу: в вызов вставляются только нужные ключи.
2a. Секция и её оформление
const page = await figma.getNodeByIdAsync("PAGE_ID");
await figma.setCurrentPageAsync(page);
const right = page.children.reduce((m, n) => Math.max(m, n.x + n.width), 0);
const section = figma.createSection();
section.name = "COMPONENT_NAME";
section.x = right + 400;
section.y = 0;
section.resizeWithoutConstraints(8000, 8000); // временно, подгонка — рецепт 7
// образец: готовая секция документации на этой же странице
const sample = page.children.find(n => n.type === "SECTION" && n.id !== section.id &&
n.children.some(c => c.name === "История изменений"));
if (sample) {
section.fills = sample.fills;
section.strokes = sample.strokes;
section.strokeWeight = sample.strokeWeight;
section.strokeAlign = sample.strokeAlign;
const r = sample.boundVariables && sample.boundVariables.topLeftRadius;
if (r) {
const v = await figma.variables.getVariableByIdAsync(r.id);
for (const c of ["topLeftRadius", "topRightRadius", "bottomLeftRadius", "bottomRightRadius"])
section.setBoundVariable(c, v);
}
} else {
const V = {
base: "27e361c2f3293fcb62995b9c94b1eb22b64490d3", // color/section/ds-section-02
film: "fe38f9ec9248426ff35d8322cb882f61b23ea523", // color/section/ds-section-01
stroke: "e3ac4487860ac9fdce1eaee3f42c678ca573cf19", // color/ds-tertiary
radius: "6c0eb5739ed61e6eebcc4dea47f415a74e8119a1" // radius/ds-radius-section
};
const v = {};
for (const k of Object.keys(V)) v[k] = await figma.variables.importVariableByKeyAsync(V[k]);
const solid = op => ({ type: "SOLID", color: { r: 0, g: 0, b: 0 }, opacity: op });
section.fills = [
figma.variables.setBoundVariableForPaint(solid(1), "color", v.base),
figma.variables.setBoundVariableForPaint(solid(0.01), "color", v.film)
];
section.strokes = [figma.variables.setBoundVariableForPaint(solid(0.4), "color", v.stroke)];
section.strokeWeight = 1;
section.strokeAlign = "INSIDE";
for (const c of ["topLeftRadius", "topRightRadius", "bottomLeftRadius", "bottomRightRadius"])
section.setBoundVariable(c, v.radius);
}
return { sectionId: section.id, styledFrom: sample ? sample.id : "keys" };
setBoundVariableForPaint возвращает новую заливку — результат обязательно присвоить.
3. Страница: импорт → открепление → в секцию
Один вызов на страницу. После return — снимок.
const page = await figma.getNodeByIdAsync("PAGE_ID");
await figma.setCurrentPageAsync(page);
const section = await figma.getNodeByIdAsync("SECTION_ID");
// …импорт и шрифты из рецепта 2…
const pc = await figma.importComponentByKeyAsync("PAGE_KEY");
const frame = pc.createInstance().detachInstance(); // → FrameNode
section.appendChild(frame);
frame.name = pc.name; // «История изменений», «Спецификация», «❖ Компонент»
const PAD = 100, GAP = 100;
const others = section.children.filter(c => c.id !== frame.id);
frame.x = others.reduce((m, c) => Math.max(m, c.x + c.width + GAP), PAD);
frame.y = PAD;
const content = frame.findChild(n => n.name === "Content");
// снять геометрию демо-блоков ДО удаления — её копируют новые блоки
const demo = content.children.map(c => ({ name: c.name, type: c.type, id: c.id,
gap: c.itemSpacing, sizingH: c.layoutSizingHorizontal }));
return { frameId: frame.id, contentId: content.id, demo };
Демо удаляется в том же вызове, где ставятся свои блоки: пустой Content между вызовами
не нужен. «Спецификация» и «❖ Компонент» используют демо как образец (рецепты 5 и 6),
поэтому удаляют его сами.
4. «История изменений»
// …страница, импорт S.log, шрифты…
const content = await figma.getNodeByIdAsync("CONTENT_ID");
const demo = content.children.slice(); // демо ds-log шаблона
const sizingH = demo.length ? demo[0].layoutSizingHorizontal : "FILL";
const log = variantOf(S.log, { "Релизовано": "False" }).createInstance();
content.appendChild(log); // новая запись — ВНИЗ
const fresh = content.children[content.children.length - 1];
fresh.layoutSizingHorizontal = sizingH;
// СНАЧАЛА свойства самого ds-log: слой «Дата» появляется в инстансе,
// только когда «Дата изменения» = true (проверено 22.10.2026 — иначе findOne вернёт null)
setProps(fresh, { "Описание": false, "Дата изменения": true });
// запись с текстом: { "Описание": true, "Описание ": nb("…") } — второе имя с пробелом
const inner = name => fresh.findOne(n => n.type === "INSTANCE" && n.name === name);
setProps(inner("Версия"), { major: "MAJOR", minor: "MINOR", patch: "PATCH" });
setProps(inner("Дата"), { Day: "DD", Month: "MM", Year: "YY" }); // с ведущими нулями
setProps(inner("Тип изменения"), { "Тип изменения": "Новая версия" });
for (const d of demo) d.remove();
return { logId: fresh.id };
Дату передаёт агент строкой — так результат не зависит от часов машины.
Пересборка. Записи старой секции переносятся копиями, по порядку, до новой записи:
const old = await figma.getNodeByIdAsync("OLD_LOG_CONTENT_ID");
for (const entry of old.children) {
if (entry.type !== "INSTANCE") continue;
content.appendChild(entry.clone());
}
Текущая версия — Версия последней записи:
function readVersion(log) {
const v = log.findOne(n => n.type === "INSTANCE" && n.name === "Версия");
const get = name => Number(v.componentProperties[propKey(v, name, "TEXT")].value);
return { major: get("major"), minor: get("minor"), patch: get("patch") };
}
function bump(prev, type) {
if (type === "Новая версия") return { major: prev.major + 1, minor: 0, patch: 0 };
if (type === "Доработка компонента") return { major: prev.major, minor: prev.minor + 1, patch: 0 };
return { major: prev.major, minor: prev.minor, patch: prev.patch + 1 }; // Исправление бага
}
5. «Спецификация»
Три вызова и больше: лид со «Структурой», затем блоки свойств — по два-три блока за вызов, чтобы ответ и ошибка оставались обозримыми.
5.1 Лид и «Структура»
// …страница, импорт S.para, S.mc, шрифты…
const content = await figma.getNodeByIdAsync("CONTENT_ID");
const groups = content.children.filter(c => c.name === "Group");
const structure = groups[0]; // ds-paragraph-dev-mode + ds-mcp-component View=Anatomy
const proto = groups[1]; // образец блока свойства: заголовок + View=State
proto.name = "__proto"; // удаляется после последнего блока (5.2)
// лид — первым в Content, без заголовка
const lead = variantOf(S.para, { Type: "H1" }).createInstance();
content.insertChild(0, lead);
lead.layoutSizingHorizontal = "FILL";
setProps(lead, { "Show Title": false, "Description": nb("LEAD") });
// анатомия: демо в слоте — простой FRAME, чистится обычным циклом
const anatomy = structure.findOne(n => n.type === "INSTANCE" && n.name === "ds-mcp-component");
const slot = anatomy.findOne(n => n.type === "SLOT" && n.name === "Slot Container");
while (slot.children.length) slot.children[0].remove();
const sample = (await figma.getNodeByIdAsync("DEFAULT_VARIANT_ID")).createInstance();
slot.appendChild(sample);
const example = slot.children[slot.children.length - 1];
setProps(anatomy, { "Show Title": false, "Show Desciption": false });
return { leadId: lead.id, anatomyId: anatomy.id, exampleId: example.id, protoId: proto.id };
Аннотации на example — отдельным вызовом по annotations.md: после setProps(anatomy)
id внутри инстанса могли смениться, поэтому слои ищутся заново от ANATOMY_ID.
5.2 Блок свойства
// …страница, импорт S.para, S.mc, S.state, шрифты…
const content = await figma.getNodeByIdAsync("CONTENT_ID");
const proto = await figma.getNodeByIdAsync("PROTO_ID");
// BLOCK = { title, desc, rows: [{ label, variantId, bool: { name, value } | null, position }] }
const BLOCK = BLOCK_JSON;
// группа копирует раскладку образца — без чисел
const group = proto.clone();
group.name = "Group";
content.insertChild(content.children.indexOf(proto), group); // перед образцом
for (const c of group.children.slice()) c.remove();
const h = variantOf(S.para, { Type: "H1" }).createInstance();
group.appendChild(h);
h.layoutSizingHorizontal = "FILL";
const host = variantOf(S.mc, { View: "State" }).createInstance();
group.appendChild(host);
host.layoutSizingHorizontal = "FILL";
// 1. хирургия слотов — до любого setProperties
const slot = host.findOne(n => n.type === "SLOT" && n.name === "Slot Container State");
clearSlotWithMarker(slot); // демо — инстансы ds-mcp-component-state; функция — 5.3, вставить в вызов
for (const row of BLOCK.rows) {
const r = variantOf(S.state, { Position: row.position }).createInstance();
slot.appendChild(r);
const fresh = slot.children[slot.children.length - 1];
fresh.layoutSizingHorizontal = "FILL";
const inner = fresh.findOne(n => n.type === "SLOT" && n.name === "Slot Container");
while (inner.children.length) inner.children[0].remove(); // демо — один ds-slot
const ex = (await figma.getNodeByIdAsync(row.variantId)).createInstance();
if (row.bool) setProps(ex, { [row.bool.name]: row.bool.value });
inner.appendChild(ex);
}
// 2. свойства — от листьев к корню: строки → заголовок → хозяин
slot.children.forEach((r, i) => setProps(r, {
"Type": nb(BLOCK.rows[i].label), "Show Description": false }));
setProps(h, { "Title": nb(BLOCK.title), "Description": nb(BLOCK.desc),
"Show Title": true, "Show Description": true });
setProps(host, { "Show Title": false, "Show Desciption": false });
return { groupId: group.id, rows: slot.children.map(r => r.id) };
После последнего блока — proto.remove().
Выбор варианта для строки (row.variantId) агент делает по инвентарю, до вызова:
// default — variantProperties мастера по умолчанию; axis — свойство блока; value — значение
function pick(variants, def, axis, value) {
const same = variants.filter(v => v.p[axis] === value);
let best = null, score = -1;
for (const v of same) {
const s = Object.keys(def).filter(k => k !== axis && v.p[k] === def[k]).length;
if (s > score) { best = v; score = s; }
}
return { id: best ? best.id : null,
exact: best ? score === Object.keys(def).length - 1 : false };
}
exact: false — строка в отчёт как разреженная. id: null — строки нет, в отчёт.
Для BOOLEAN обе строки берут мастер по умолчанию, а значение ставится на инстанс
(row.bool).
Position: Horizontal, если ширина варианта меньше 320, иначе Vertical.
5.3 Очистка слота
Обычный цикл — когда демо в слоте простые узлы или инстансы другого компонента:
Slot Container у View=Anatomy (FRAME Button), Slot Container строки (ds-slot).
while (slot.children.length) slot.children[0].remove();
Метка — когда демо — инстансы того же компонента, который сейчас добавится:
Slot Container State (демо — строки ds-mcp-component-state). Обычный цикл там падает
на in remove: Node not found: после первого remove() остальные демо становятся фантомами.
function clearSlotWithMarker(slot) {
const marker = figma.createFrame();
marker.name = "__MARKER__";
marker.resize(1, 1);
slot.appendChild(marker);
while (true) {
const d = slot.children.find(c => c.name !== "__MARKER__");
if (!d) break;
d.remove();
}
slot.children.find(c => c.name === "__MARKER__").remove();
}
Пустой слот демо не возвращает. slot.resetSlot() слот не очищает — возвращает демо.
6. «❖ Компонент»
Два вызова: перенос набора и шапка с подвалом, затем подписи осей.
6.1 Перенос набора, шапка, подвал
// …страница, шрифты…
const frame = await figma.getNodeByIdAsync("COMP_FRAME_ID");
const src = await figma.getNodeByIdAsync("SET_ID");
const from = src.parent.name + " (" + src.parent.type + ")";
const grid = frame.findOne(n => n.name === "Components" && n.layoutMode === "GRID");
const box = grid.findChild(n => n.name === "Components");
const container = box.findChild(n => n.name === "Container");
for (const c of container.children.slice()) c.remove();
container.appendChild(src); // набор — единственный узел
const title = frame.findOne(n => n.type === "INSTANCE" && n.name === "ds-format-component-head-title");
setProps(title, { "Название компонента": src.name });
const footer = frame.findOne(n => n.type === "INSTANCE" && n.name === "ds-format-component-footer");
const ver = footer.findOne(n => n.type === "INSTANCE" && n.name === ".ds-log-changelog-version");
setProps(ver, { major: "MAJOR", minor: "MINOR", patch: "PATCH" });
return { from, setId: src.id, gridId: grid.id };
Слой версии в подвале может называться иначе — тогда искать по
n.mainComponent.name === ".ds-log-changelog-version".
6.2 Оси по геометрии набора
Агент считает уровни по инвентарю, до вызова:
// variants: [{ x, y, w, h, p }], axes: имена VARIANT-свойств
function groups(items, pos, size) { // варианты с пересекающимся диапазоном — одна группа
const s = items.slice().sort((a, b) => a[pos] - b[pos]);
const out = [];
for (const it of s) {
const last = out[out.length - 1];
if (last && it[pos] < last.end) { last.items.push(it); last.end = Math.max(last.end, it[pos] + it[size]); }
else out.push({ start: it[pos], end: it[pos] + it[size], items: [it] });
}
return out;
}
function constant(gs, axis) { return gs.every(g => g.items.every(it => it.p[axis] === g.items[0].p[axis])); }
function runs(gs, axis) { // соседние группы с одним значением сливаются в одну скобку
const out = [];
for (const g of gs) {
const v = g.items[0].p[axis], last = out[out.length - 1];
if (last && last.value === v) last.end = g.end;
else out.push({ value: v, start: g.start, end: g.end });
}
return out;
}
const cols = groups(variants, "x", "w"), rows = groups(variants, "y", "h");
const H = [], V = [], unplaced = [];
for (const axis of axes) {
if (cols.length > 1 && constant(cols, axis) && runs(cols, axis).length > 1) H.push({ axis, runs: runs(cols, axis) });
else if (rows.length > 1 && constant(rows, axis) && runs(rows, axis).length > 1) V.push({ axis, runs: runs(rows, axis) });
else unplaced.push(axis);
}
H.sort((a, b) => a.runs.length - b.runs.length); // внешний уровень — первым
V.sort((a, b) => a.runs.length - b.runs.length);
Вызов ставит подписи. Координаты переводятся через абсолютные позиции, поэтому отступы сетки не важны:
// …страница, импорт S.label, шрифты…
const grid = await figma.getNodeByIdAsync("GRID_ID");
const src = await figma.getNodeByIdAsync("SET_ID");
const hp = grid.findChild(n => n.name === "Horizontal Props");
const vp = grid.findChild(n => n.name === "Vertical Props");
const H = H_JSON, V = V_JSON; // из расчёта выше
for (const c of hp.children.slice()) c.remove();
for (const c of vp.children.slice()) c.remove();
const ax = n => n.absoluteTransform[0][2], ay = n => n.absoluteTransform[1][2];
function level(parent, horizontal) {
const f = figma.createFrame();
f.name = "Variable"; f.fills = []; f.clipsContent = false;
parent.appendChild(f);
return f;
}
function label(text, large, vertical) {
const i = variantOf(S.label, { Large: large ? "True" : "False", Vertical: vertical ? "True" : "False" })
.createInstance();
setProps(i, { "Label": text });
return i;
}
H.forEach((lvl, idx) => {
const row = level(hp, true);
let maxH = 0;
const made = lvl.runs.map(r => { const l = label(r.value, idx === 0, false); row.appendChild(l); maxH = Math.max(maxH, l.height); return [l, r]; });
row.resize(Math.max(1, src.width + (ax(src) - ax(row))), maxH);
for (const [l, r] of made) { l.resize(Math.max(1, r.end - r.start), l.height); l.x = ax(src) - ax(row) + r.start; l.y = 0; }
});
// COL_PAD — paddingLeft внутреннего «Variable» демо-столбца, снятый ДО удаления демо (в шаблоне 16)
V.forEach((lvl, idx) => {
const col = level(vp, false);
const made = lvl.runs.map(r => {
const l = label(r.value, idx === 0, true);
col.appendChild(l);
l.primaryAxisSizingMode = "AUTO"; // у атома ширина фиксирована — длинный текст вылезает влево
return [l, r];
});
const w = made.reduce((m, p) => Math.max(m, p[0].width), 1) + COL_PAD;
col.resize(w, Math.max(1, src.height + (ay(src) - ay(col))));
for (const [l, r] of made) {
l.resize(l.width, Math.max(1, r.end - r.start));
l.primaryAxisSizingMode = "AUTO"; // resize сбрасывает режим в FIXED
l.x = w - l.width; // скобкой к набору
l.y = ay(src) - ay(col) + r.start;
}
});
return { rows: hp.children.map(c => c.id), cols: vp.children.map(c => c.id) };
Снимок после вызова обязателен: скобка каждой подписи должна накрывать свою группу вариантов.
Подпись переносит текст посреди слова — подписи не хватает ширины: у текста
textAutoResize = "WIDTH_AND_HEIGHT", у подписи — ширина группы, но не меньше ширины текста.
7. Подгонка секции
Последний вызов, когда высоты страниц окончательные.
const section = await figma.getNodeByIdAsync("SECTION_ID");
const PAD = 100;
let maxR = 0, maxB = 0;
for (const c of section.children) { maxR = Math.max(maxR, c.x + c.width); maxB = Math.max(maxB, c.y + c.height); }
section.resizeWithoutConstraints(Math.ceil(maxR + PAD), Math.ceil(maxB + PAD));
return { w: section.width, h: section.height };
8. Ловушки
Component with key … not found | библиотека не включена в файле или атом удалён | Assets → Libraries; иначе обновить engine-map.md |
вложенный .ds-log-* по ключу не импортируется | приватный атом, не опубликован | доставать через родителя: findOne по имени слоя |
| поиск по имени нашёл не тот атом | имена в библиотеке задвоены | только ключи |
свойство «Описание»: найдено 2 | у ds-log два свойства, отличаются пробелом | имя точно, с типом: Описание — BOOLEAN, Описание — TEXT |
Show Description у ds-mcp-component не найден | опечатка в атоме | Show Desciption |
Cannot write to node with unloaded font | шрифт не загружен | рецепт 2 в начале каждого вызова, который пишет |
setProperties на слот падает | слот задаётся детьми | appendChild в узел SLOT |
in remove: Node not found в Slot Container State | демо — инстансы того же атома | метка, рецепт 5.3 |
то же после setProperties | любой setProperties пересоздаёт id поддерева | вся хирургия слотов до свойств; свойства — от листьев к корню |
Internal Figma Error: Parent not found | ссылка устарела после appendChild | перечитать через parent.children |
FILL can only be set on children of auto-layout frames | layoutSizing* до appendChild | сначала родитель, потом размер |
| инстанс в слоте вылез за край | после вставки он FIXED | layoutSizingHorizontal = "FILL" у строк; у примера компонента — нет |
| страница не принимает новые блоки | это инстанс, а не фрейм | detachInstance() обёртки страницы |
| импортированный атом пропал между вызовами | id импорта живёт один вызов | импортировать заново в каждом вызове |
node.screenshot() мастера внутри набора пустой | особенность рендера | снимать набор или страницу целиком |
figma.createFrame() режет содержимое | по умолчанию clipsContent: true | сбросить вручную |