Construcción en Figma
Aquí todo falló silenciosamente primero. Nada lanza excepciones, nada advierte, y el lienzo simplemente se ve mal.
El bucle, antes que nada
Sella la compilación y limpia tu propia salida. De lo contrario, puedes depurar "se ve igual" durante mucho tiempo cuando Figma está sirviendo un paquete en caché.
const BUILD = "15:03"; // aumentar en cada paquete
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 + " — replaced " + removed + " frames.");
Dos números en el mensaje, y ambos son de diagnóstico. Un sello desactualizado significa que el
paquete está en caché: elimina el plugin, vuelve a importar el manifiesto, vuelve a abrir la pestaña.
Replaced 0 con marcos visibles en el lienzo significa lo mismo.
Sin esto, un plugin que recrea marcos apila un segundo conjunto encima del primero y nada te dice cuál estás viendo. Contar copias de un nombre de marco es cómo demuestras si una ejecución ocurrió realmente.
Limpiar y reconstruir es solo para las primeras compilaciones. Una vez que un archivo está en revisión, los comentarios están fijados a los ID de nodo. Eliminar un marco y dibujar uno nuevo deja huérfanos todos los comentarios que tenía. Durante una ronda de revisión, edita los nodos existentes en su lugar (ver El bucle de revisión).
Un valor incorrecto arruina toda la compilación
Cada fallo en este archivo tiene la misma forma: un solo valor que la API rechaza, lanzado a medio dibujo, dejando una página medio renderizada que se lee como un problema de diseño. Culpables típicos: un ancho negativo, una altura de texto fija de 10px, un peso de fuente no cargado.
Aplica límites en el límite en lugar de confiar en la aritmética.
function W1(v, min){ const n = Number(v); return (isFinite(n) && n > 0) ? n : (min || 1); }
Haz pasar cada ancho, altura y ancho de texto a través de él. Un límite cuesta una caja con aspecto incorrecto; un lanzamiento cuesta la página.
La aritmética que muerde es una resta fija que solo se cumple en un ancho:
const FEED = DW - PAD*2 - 400; // 840 a 1440. MENOS 50 a 390.
Cualquier constante restada del ancho del lienzo necesita un guarda de punto de interrupción, o se vuelve negativa en el pequeño.
Síntoma a reconocer: los marcos anchos se ven terminados y los estrechos están ausentes o medio dibujados, superponiéndose a algo más. Eso no es un error de posicionamiento. Lee el mensaje de error: nombra la propiedad y el nodo.
Nunca dibujes en el origen
Un plugin que comienza en (0, 0) cae encima de lo que ya está allí, y
un limpiador que solo elimina sus propios marcos no te salvará: dos plugins
compartiendo una página ambos creen que el lienzo está vacío.
Mide primero, luego comienza limpio:
let startY = 0;
figma.currentPage.children.forEach(n => { startY = Math.max(startY, n.y + n.height); });
if (figma.currentPage.children.length) startY += 600;
Etiqueta cada fila en el lienzo, y haz que el limpiador coincida con todo lo que la compilación emite (marcos y etiquetas), o al volver a ejecutar quedarán textos huérfanos atrás.
Tamaño del texto, en este orden exacto
El error más costoso. Figma recalcula la altura de un nodo de texto cuando su contenido cambia, no cuando lo redimensionas.
const str = String(s);
const t = figma.createText();
t.fontName = { family: "Inter", style: weight };
t.characters = str; // 1. PRIMERO
t.fontSize = size; // 2. estilos después
t.lineHeight = { unit: "PERCENT", value: 145 };
if (width) {
t.textAutoResize = "HEIGHT"; // 3. NO se mantiene en un nodo vacío
t.resize(width, t.height); // nunca una altura literal
t.characters = str; // 4. forzar una nueva medición con el nuevo ancho
} else {
t.textAutoResize = "WIDTH_AND_HEIGHT";
}
textAutoResize = "HEIGHT"establecido antes de que el nodo tenga caracteres se descarta. Reordenar para poner el contenido al final sin mover la propiedad lo empeora.resize(w, 10)fija la altura en 10 para siempre. Pasat.height.- Reasignar la misma cadena es la doble garantía de remedición.
Síntoma: cada texto con un ancho explícito mide ~10px de alto, cada texto sin una es correcto, y cada marco cuya altura proviene solo del texto colapsa a su relleno.
width significa ancho fijo — no es obvio qué eje es
if (opts.horizontal) {
f.primaryAxisSizingMode = width ? "FIXED" : "AUTO"; // width ES el eje principal
f.counterAxisSizingMode = "AUTO";
} else {
f.counterAxisSizingMode = width ? "FIXED" : "AUTO"; // width es el eje contra
f.primaryAxisSizingMode = "AUTO";
}
Establecer counterAxisSizingMode en un marco horizontal fija su altura y
deja el ancho ajustándose al contenido. Cada fila construida de esa manera no tiene ancho libre, por lo que
ningún hijo puede crecer dentro de ella.
Redimensiona antes de establecer los modos de tamaño, o el tamaño explícito lucha contra el ajuste al contenido.
Una columna que llena una fila
function growCol(n) { n.layoutGrow = 1; n.counterAxisSizingMode = "FIXED"; return n; }
Un marco vertical creado con width: null se ajusta al contenido. Dale layoutGrow = 1 y
hijos con ESTIRAR y tienes una restricción circular: el padre se ajusta a los
hijos, los hijos se ajustan al padre. Figma lo resuelve al mínimo.
Síntoma: tarjetas de ~45px de ancho, texto del cuerpo ajustándose un carácter por línea,
nodos de texto informando 1 x 988. Marcos de página tres veces más altos de lo que deberían,
enteramente por espacio vertical falso.
Prefiere pasar a las filas un ancho explícito en lugar de confiar en ESTIRAR para resolverlo.
Mide, no mires
La geometría responde la pregunta más rápido que una captura de pantalla, y expone todos los errores
anteriores. Obtén get_metadata, luego:
- alturas ≤ 12px → texto o marcos colapsados
- anchos de 1 → la restricción circular anterior
- una página mucho más alta que su equivalente HTML → ajuste estrecho, no contenido
- una copia de cada marco cuando esperabas dos → la ejecución nunca ocurrió
Verifica los totales: los rangos y de un bloque deberían encajar en su página sin espacios, y el último bloque debería terminar exactamente en la altura de la página.
Simulando el plugin localmente
tools/figma_stub.js en esta habilidad es un stub estricto listo para usar; node tools/test_figma_stub.js demuestra que aún lanza donde Figma lanza.
Un stub del objeto global figma detecta lanzamientos, agregados nulos y errores
estructurales antes de un viaje de ida y vuelta. No puede emular métricas de texto, por lo que demuestra que el
código se ejecuta, nunca que el diseño sea correcto.
Haz el stub tan estricto como la API real, o pasará compilaciones que mueren en Figma. Cada uno de estos es un error que llegó al lienzo porque el stub era tolerante:
resize(w, h){
if (!(w >= 0)) throw new Error('in resize: Property "width" failed validation');
if (!(h >= 0)) throw new Error('in 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"); ... }
Dale al stub currentPage.children, y siembra con los marcos que la página real
ya contiene — así es como una colisión de origen se muestra localmente.
Para verificar el posicionamiento, el stub también necesita calcular alturas de diseño automático: suma
los hijos más itemSpacing y el relleno para un marco VERTICAL, toma el máximo
para uno HORIZONTAL, y devuelve el valor fijo cuando
primaryAxisSizingMode === "FIXED". Aproximado es suficiente para detectar superposición de filas.
Ejecuta cada ruta de envío, cada punto de interrupción y cada configuración de fuente, no solo la feliz:
Inter solo · Inter + mono · los tres · Inter Regular solo
escritorio · móvil oscuro · claro
Fuentes: carga lo que pedirás y resuelve lo que obtienes
Asignar una fuente no cargada lanza un error. Dos trampas detrás de eso:
- Debes precargar cada peso que el código solicite. Cargar Regular, Medium y Bold y luego solicitar Semi Bold es un fallo esperando a la única tarjeta que lo usa.
- El nombre del estilo difiere según la familia. Inter dice
"Semi Bold"; JetBrains Mono dice"SemiBold". Pedir el peso correcto con la ortografía incorrecta aún lanza.
Prueba cada peso por separado, registra lo que realmente se cargó, y resuelve cada solicitud a través de una cadena de respaldo en lugar de asumir:
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 precargues estrictamente en el punto de entrada con Promise.all — un peso
faltante allí mata una compilación que el resolvedor habría manejado. Informa las
familias a las que resolviste en el mensaje de cierre.
Un plugin, un trabajo
Resiste un switch de envío dentro de un solo plugin — según el tipo de editor, nombre de página o selección. Un switch por nombre de página significa que el usuario debe realizar un ritual antes de que funcione, y cuando silenciosamente hace lo incorrecto no hay nada que leer.
Dos manifiestos con diferentes valores de id y name no cuestan nada y eliminan toda la
clase de problema. Figma también reutiliza un registro en caché cuando un
manifiesto reimportado lleva un id que ya conoce, por lo que cambiar el id es
cómo se fuerza una carga limpia.
Leyendo comentarios de vuelta
El MCP expone get_metadata, get_design_context, get_screenshot,
get_figjam, use_figma — ninguno de ellos lee comentarios, y la API de Plugins
tampoco tiene acceso a comentarios. Los comentarios viven en la API REST.
Usa tools/figma_comments.py en esta carpeta de habilidad (Python 3, biblioteca estándar):
T=~/.claude/skills/figma-build/tools/figma_comments.py
python3 $T check <file-url> # OK: token lee comentarios (N en file KEY)
python3 $T fetch <file-url> # hilos abiertos como JSON, ledger y resueltos filtrados
python3 $T mark <file-url> ID... --change "lo que cambió"
python3 $T reply <file-url> ROOT_ID "Hecho: logo 20% más grande" # necesita el alcance de escritura
Lee el token de ~/.figma-token y nunca lo imprime. Ejecútalo desde la carpeta de trabajo
para que el ledger aterrice allí, o establece FIGMA_LEDGER. No hay comando de eliminación.
Si informa un error de certificado SSL, el Python no tiene certificados raíz:
ejecuta /usr/bin/python3 en su lugar, o pip install certifi.
Alcance: Comentarios de archivo → Leer. Almacena el token en un archivo (~/.figma-token,
chmod 600), nunca en la conversación. Si la persona no puede ejecutar un comando de shell,
escribe el archivo con un marcador de posición y open -a TextEdit para que lo peguen.
/v1/me devuelve 403 en un token con alcance de comentarios. Eso es esperado, no un
token roto. /v1/files/:key/nodes también da 403 sin file_content:read.
Cada comentario lleva client_meta:
{ "node_id": "2:95", "node_offset": { "x": 279, "y": 1257 } }
Mapea a un bloque obteniendo get_metadata y encontrando el marco hijo cuyo
rango y contiene el desplazamiento. Los hilos vienen planos: agrupa por parent_id, y
las respuestas a menudo llevan la instrucción real mientras que la raíz es solo un puntero.
Los comentarios son por archivo. Mover contenido a un nuevo archivo los deja atrás.
El bucle de revisión, cuando te pidan "lee mis comentarios"
- Obtener con
figma_comments.py fetch, que agrupa comentarios en hilos. Salta los resueltos (conresolved_atestablecido) y cada ID ya en el ledger manejado (abajo). Los comentarios antiguos permanecen en el archivo hasta que se resuelven, por lo que sin estos dos filtros cada revisión, y cada conversación posterior, reaplica los comentarios de la ronda anterior. - Mapear cada hilo a su marco a través de
client_meta, como arriba. - Confirmar antes de dibujar: lista cada comentario, el marco al que se mapea y el cambio planeado, en una tabla corta. Un comentario fijado en lienzo vacío, o uno que es ambiguo, recibe una pregunta en lugar de una suposición.
- Aplicar los cambios en el lugar: cambia las propiedades de los nodos existentes, manteniendo sus IDs. Nunca elimines y redibujes un marco comentado, porque eso deja huérfanos sus comentarios. A través del MCP (
use_figma) el lienzo se actualiza directamente. A través de un plugin de desarrollo, edita su código en el disco, aumenta el sello de compilación y pide a la persona que ejecute el plugin nuevamente. - Verificar con
get_metadatay una captura de pantalla (ver Mide, no mires). En la ruta del plugin, verifica que el mensaje de cierre muestre el nuevo sello antes de confiar en el lienzo. - Informar comentario por comentario qué cambió. Si el token tiene el alcance de escritura y la persona lo desea, responde en cada hilo.
- Registrar cada ID de comentario manejado (raíces de hilos y respuestas) con
figma_comments.py markantes de que termine el turno, incluyendo aquellos a los que solo respondiste con una pregunta. - Nunca elimines comentarios, incluso cuando te pidan limpiar. Son el registro de revisión. La retroalimentación antigua se mantiene fuera de rondas posteriores a través del ledger, no eliminándola. Si una ronda necesita limpieza en Figma, la persona resuelve los comentarios.
El ledger manejado. Un archivo JSON junto al trabajo: figma/comments-handled.json en una carpeta de campaña, de lo contrario figma-comments-handled.json en el directorio de trabajo. Léelo al comienzo de cada revisión, incluso en una conversación nueva.
{ "<file_key>": { "<comment_id>": { "handled_at": "2026-11-03T14:05:00Z", "change": "logo 20% más grande en 1080x1920" } } }
No hay un endpoint de API para resolver un comentario, por lo que resolver queda en manos de la persona.
Cuándo pedir una reimportación (solo ruta de plugin): cuando el mensaje muestra un sello desactualizado, o "replaced 0" mientras hay marcos visibles. Pídeles que eliminen el plugin, importen su manifest.json nuevamente y vuelvan a abrir la pestaña. Si eso falla, cambia el id del manifiesto (ver Un plugin, un trabajo).
Capturas de pantalla a escala
Una captura por página más recortes locales es mejor que una llamada por bloque. La altura y/ del bloque
proviene de get_metadata, maxDimension igual o por encima de la altura del marco da 1:1,
y los recortes se alinean exactamente.