Figma build
Tudo aqui falhou silenciosamente primeiro. Nada lança exceções, nada avisa, e a tela simplesmente parece errada.
O loop, antes de qualquer coisa
Carimbe a construção e limpe sua própria saída. Caso contrário, você pode depurar "parece igual" por um longo tempo quando o Figma está servindo um pacote em cache.
const BUILD = "15:03"; // incremente a cada pacote
function clearPrevious() {
const mine = figma.currentPage.children.filter(n => /^\d\d · /.test(n.name));
mine.forEach(n => n.remove());
return mine.length;
}
figma.closePlugin("Build " + BUILD + " — substituídas " + removed + " frames.");
Dois números no toast, e ambos são diagnósticos. Um carimbo desatualizado significa que o
pacote está em cache: remova o plugin, reimporte o manifesto, reabra a aba.
Substituídas 0 com quadros visíveis na tela significa a mesma coisa.
Sem isso, um plugin que recria quadros empilha um segundo conjunto sobre o primeiro e nada te diz qual deles você está vendo. Contar cópias de um nome de quadro é como você prova se uma execução aconteceu ou não.
Limpar e reconstruir é apenas para as primeiras construções. Depois que um arquivo está em revisão, os comentários são fixados em IDs de nós. Remover um quadro e desenhar um novo órfão todo comentário nele. Durante uma rodada de revisão, edite nós existentes no lugar (veja O loop de revisão).
Um valor ruim quebra toda a construção
Toda falha neste arquivo tem a mesma forma: um único valor que a API rejeita, lançado no meio do desenho, deixando uma página meio renderizada que parece um problema de layout. Culpados típicos: largura negativa, altura fixa de 10px em texto, peso de fonte não carregado.
Use clamping no limite em vez de confiar na aritmética.
function W1(v, min){ const n = Number(v); return (isFinite(n) && n > 0) ? n : (min || 1); }
Direcione cada largura, altura e largura de texto por ele. Um clamp custa uma caixa com aspecto errado; um throw custa a página.
A aritmética que pega é uma subtração fixa que só funciona em uma largura:
const FEED = DW - PAD*2 - 400; // 840 em 1440. MENOS 50 em 390.
Qualquer constante subtraída da largura da tela precisa de um guarda de ponto de quebra, ou vai ficar negativa na tela pequena.
Sintoma a reconhecer: os quadros largos parecem prontos e os estreitos estão ausentes ou meio desenhados, sobrepondo outra coisa. Isso não é um bug de posicionamento. Leia o toast de erro — ele nomeia a propriedade e o nó.
Nunca desenhe na origem
Um plugin que começa em (0, 0) cai sobre o que já está lá, e
um limpador que só remove seus próprios quadros não vai te salvar: dois plugins
compartilhando uma página acreditam que a tela está vazia.
Meça primeiro, então comece limpo:
let startY = 0;
figma.currentPage.children.forEach(n => { startY = Math.max(startY, n.y + n.height); });
if (figma.currentPage.children.length) startY += 600;
Rotule cada linha na tela, e faça o limpador corresponder a tudo que a construção emite — quadros e rótulos — ou reexecutar deixará texto órfão para trás.
Dimensionamento de texto, nesta ordem exata
O erro mais caro. O Figma recalcula a altura de um nó de texto quando seu conteúdo muda, não quando você o redimensiona.
const str = String(s);
const t = figma.createText();
t.fontName = { family: "Inter", style: weight };
t.characters = str; // 1. PRIMEIRO
t.fontSize = size; // 2. estilos depois
t.lineHeight = { unit: "PERCENT", value: 145 };
if (width) {
t.textAutoResize = "HEIGHT"; // 3. NÃO funciona em um nó vazio
t.resize(width, t.height); // nunca uma altura literal
t.characters = str; // 4. força uma remedição na nova largura
} else {
t.textAutoResize = "WIDTH_AND_HEIGHT";
}
textAutoResize = "HEIGHT"definido antes do nó ter caracteres é ignorado. Reordenar para colocar o conteúdo por último sem mover a propriedade piora.resize(w, 10)fixa a altura em 10 para sempre. Passet.height.- Reatribuir a mesma string é a medida de segurança dupla.
Sintoma: todo texto com largura explícita tem ~10px de altura, todo texto sem largura explícita está correto, e todo quadro cuja altura vem apenas de texto colapsa para seu padding.
width significa largura fixa — qual eixo não é óbvio
if (opts.horizontal) {
f.primaryAxisSizingMode = width ? "FIXED" : "AUTO"; // largura É o eixo primário
f.counterAxisSizingMode = "AUTO";
} else {
f.counterAxisSizingMode = width ? "FIXED" : "AUTO"; // largura é o eixo contrário
f.primaryAxisSizingMode = "AUTO";
}
Definir counterAxisSizingMode em um quadro horizontal fixa sua altura e
deixa a largura ajustada ao conteúdo. Cada linha construída assim não tem largura
sobrando, então nenhum filho pode crescer nela.
Redimensione antes de definir os modos de dimensionamento, ou o tamanho explícito luta contra o ajuste ao conteúdo.
Uma coluna que preenche uma linha
function growCol(n) { n.layoutGrow = 1; n.counterAxisSizingMode = "FIXED"; return n; }
Um quadro vertical criado com width: null ajusta ao conteúdo. Dê a ele layoutGrow = 1 e
filhos com ESTICAR e você tem uma restrição circular: o pai dimensiona para os filhos,
os filhos dimensionam para o pai. O Figma resolve isso no mínimo.
Sintoma: cartões com ~45px de largura, corpo do texto quebrando uma linha por caractere,
nós de texto relatando 1 x 988. Quadros de página três vezes mais altos do que deveriam,
inteiramente devido a espaço vertical falso.
Prefira passar larguras explícitas para linhas em vez de confiar em ESTICAR para resolver.
Meça, não olhe
A geometria responde à pergunta mais rápido que uma captura de tela, e expõe todos os bugs
acima. Use get_metadata, então:
- alturas ≤ 12px → texto ou quadros colapsados
- larguras de 1 → a restrição circular acima
- uma página muito mais alta que sua equivalente HTML → quebra estreita, não conteúdo
- uma cópia de cada quadro quando você esperava duas → a execução nunca aconteceu
Verifique a sanidade dos totais: os intervalos y de um bloco devem alinhar sua página sem lacunas, e o último bloco deve terminar exatamente na altura da página.
Simulando o plugin localmente
tools/figma_stub.js nesta skill é um stub estrito pronto; node tools/test_figma_stub.js prova que ele ainda lança onde o Figma lança.
Um stub do global figma captura throws, anexos nulos e erros estruturais
antes de uma ida e volta. Ele não pode emular métricas de texto, então prova que o
código executa, nunca que o layout está correto.
Faça o stub tão estrito quanto a API real, ou ele passará construções que morrem no Figma. Cada um destes é um bug que chegou à tela porque o stub era permissivo:
resize(w, h){
if (!(w >= 0)) throw new Error('em resize: Property "width" failed validation');
if (!(h >= 0)) throw new Error('em resize: Property "height" failed validation');
...
}
set fontName(v){
if (!loaded.has(v.family + "|" + v.style))
throw new Error('Cannot use unloaded font "' + v.family + " " + v.style + '"');
...
}
appendChild(c){ if (!c) throw new Error("null child"); ... }
Alimente o stub com currentPage.children, e semeie com os quadros que a página
real já contém — é assim que uma colisão de origem aparece localmente.
Para verificar o posicionamento, o stub também precisa calcular alturas de layout automático:
some os filhos mais itemSpacing e padding para um quadro VERTICAL, pegue o máximo
para um HORIZONTAL, e retorne o valor fixo quando
primaryAxisSizingMode === "FIXED". Grosseiro é suficiente para detectar sobreposição de linhas.
Execute todos os caminhos de despacho, todos os pontos de quebra e todas as configurações de fonte, não apenas o caminho feliz:
Inter apenas · Inter + mono · todos os três · Inter Regular apenas
desktop · mobile dark · light
Fontes: carregue o que você vai pedir, e resolva o que você obtém
Atribuir uma fonte não carregada lança exceção. Duas armadilhas por trás disso:
- Você deve pré-carregar todos os pesos que o código pedir. Carregar Regular, Medium e Bold e então solicitar Semi Bold é uma falha esperando pelo único cartão que o usa.
- O nome do estilo difere por família. Inter diz
"Semi Bold"; JetBrains Mono diz"SemiBold". Pedir o peso certo com a grafia errada ainda lança.
Teste cada peso separadamente, registre o que realmente carregou, e resolva cada requisição através de uma cadeia de fallback em vez de assumir:
const ALIAS = { "Semi Bold": ["Semi Bold","SemiBold","Medium","Bold","Regular"], … };
function font(family, style){
for (const s of ALIAS[style]) if (LOADED[family+"|"+s]) return { family, style:s };
for (const s of ALIAS[style]) if (LOADED["Inter|"+s]) return { family:"Inter", style:s };
return { family:"Inter", style:"Regular" };
}
Nunca pré-carregue estritamente no ponto de entrada com Promise.all — um peso
faltando lá mata uma construção que o resolvedor teria tratado. Relate as famílias
que você resolveu no toast de fechamento.
Um plugin, um trabalho
Resista a um switch de despacho dentro de um único plugin — por tipo de editor, nome de página, ou seleção. Um switch por nome de página significa que o usuário deve realizar um ritual antes da coisa funcionar, e quando ela silenciosamente faz a coisa errada, não há nada para ler.
Dois manifestos com diferentes id e name custam nada e removem toda a classe
de problema. O Figma também reutiliza um registro em cache quando um
manifesto reimportado carrega um id que já conhece, então mudar o id é como
você força um carregamento limpo.
Lendo comentários de volta
O MCP expõe get_metadata, get_design_context, get_screenshot,
get_figjam, use_figma — nenhum deles lê comentários, e a Plugin API
também não tem acesso a comentários. Comentários vivem na API REST.
Use tools/figma_comments.py nesta pasta da skill (Python 3, biblioteca padrão):
T=~/.claude/skills/figma-build/tools/figma_comments.py
python3 $T check <file-url> # OK: token lê comentários (N no KEY do arquivo)
python3 $T fetch <file-url> # tópicos abertos como JSON, ledger e resolvidos filtrados
python3 $T mark <file-url> ID... --change "o que mudou"
python3 $T reply <file-url> ROOT_ID "Feito: logo 20% maior" # precisa do escopo de escrita
Ele lê o token de ~/.figma-token e nunca o imprime. Execute da pasta de trabalho
para que o ledger seja salvo lá, ou defina FIGMA_LEDGER. Não há comando de exclusão.
Se reportar uma falha de certificado SSL, o Python não tem certificados raiz:
use /usr/bin/python3, ou pip install certifi.
Escopo: Comentários do arquivo → Leitura. Armazene o token em um arquivo (~/.figma-token,
chmod 600), nunca na conversa. Se a pessoa não puder executar um comando de shell,
escreva o arquivo com um placeholder e use open -a TextEdit para que ela cole.
/v1/me retorna 403 em um token com escopo de comentários. Isso é esperado, não um
token quebrado. /v1/files/:key/nodes também 403a sem file_content:read.
Cada comentário carrega client_meta:
{ "node_id": "2:95", "node_offset": { "x": 279, "y": 1257 } }
Mapeie para um bloco usando get_metadata e encontrando o quadro filho cujo
intervalo y contém o deslocamento. Os tópicos vêm planos — agrupe por parent_id, e
as respostas geralmente carregam a instrução real enquanto a raiz é apenas um ponteiro.
Comentários são por arquivo. Mover conteúdo para um novo arquivo os deixa para trás.
O loop de revisão, quando solicitado a "ler meus comentários"
- Busque com
figma_comments.py fetch, que agrupa comentários em tópicos. Pule os resolvidos (resolved_atdefinido) e todo id já no ledger de tratados (abaixo). Comentários antigos permanecem no arquivo até serem resolvidos, então sem esses dois filtros toda revisão, e toda conversa posterior, reaplica o feedback da rodada anterior. - Mapeie cada tópico para seu quadro através de
client_meta, como acima. - Confirme antes de desenhar: liste cada comentário, o quadro ao qual ele mapeia, e a mudança planejada, em uma tabela curta. Um comentário fixado em tela vazia, ou ambíguo, recebe uma pergunta em vez de um palpite.
- Aplique as mudanças no lugar: altere as propriedades dos nós existentes, mantendo seus ids. Nunca remova e redesenhe um quadro comentado, porque isso órfão seus comentários. Através do MCP (
use_figma), a tela atualiza diretamente. Através de um plugin de desenvolvimento, edite seu código no disco, incremente o carimbo de construção, e peça à pessoa para executar o plugin novamente. - Verifique com
get_metadatae uma captura de tela (veja Meça, não olhe). Na rota do plugin, verifique se o toast de fechamento mostra o novo carimbo antes de confiar na tela. - Reporte comentário por comentário o que mudou. Se o token tem o escopo de escrita e a pessoa deseja, responda em cada tópico.
- Registre todo id de comentário tratado (raízes e respostas) com
figma_comments.py markantes do turno terminar, incluindo aqueles que você apenas respondeu com uma pergunta. - Nunca delete comentários, mesmo quando solicitado a limpar. Eles são o registro da revisão. Feedback antigo fica fora das rodadas posteriores através do ledger, não por exclusão. Se uma rodada precisa ser limpa no Figma, a pessoa resolve os comentários.
O ledger de tratados. Um arquivo JSON próximo ao trabalho: figma/comments-handled.json em uma pasta de campanha, senão figma-comments-handled.json no diretório de trabalho. Leia no início de toda revisão, incluindo em uma nova conversa.
{ "<file_key>": { "<comment_id>": { "handled_at": "2026-11-03T14:05:00Z", "change": "logo 20% maior em 1080x1920" } } }
Não há endpoint de API para resolver um comentário, então resolver fica com a pessoa.
Quando pedir uma reimportação (apenas na rota do plugin): quando o toast mostra um carimbo desatualizado, ou "substituídas 0" enquanto quadros estão visíveis. Peça para remover o plugin, importar seu manifest.json novamente e reabrir a aba. Se falhar, mude o id do manifesto (veja Um plugin, um trabalho).
Capturas de tela em escala
Uma captura por página mais recortes locais é melhor que uma chamada por bloco. O y/altura do bloco
vem de get_metadata, maxDimension igual ou acima da altura do quadro dá 1:1,
e os recortes se alinham exatamente.