Figma Build
Alles hier ist zuerst stillschweigend fehlgeschlagen. Nichts wirft eine Exception, nichts warnt, und das Canvas sieht einfach falsch aus.
Die Schleife, vor allem anderen
Stemple den Build und lösche deinen eigenen Output. Sonst verbringst du viel Zeit mit dem Debuggen von „es sieht gleich aus”, während Figma ein gecachtes Bundle ausliefert.
const BUILD = "15:03"; // bei jedem Bundle erhöhen
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.");
Zwei Zahlen im Toast, und beide sind diagnostisch. Ein veralteter Stempel bedeutet, dass das Bundle gecacht ist: Entferne das Plugin, importiere das Manifest erneut, öffne den Tab neu. Replaced 0 bei sichtbaren Frames auf dem Canvas bedeutet dasselbe.
Ohne dies stapelt ein Plugin, das Frames neu erstellt, einen zweiten Satz auf den ersten – und nichts sagt dir, welchen du gerade ansiehst. Das Zählen von Kopien eines Frame-Namens ist der Beweis dafür, ob ein Durchlauf überhaupt stattgefunden hat.
Lösche-und-neu-aufbauen ist nur für den ersten Build gedacht. Sobald eine Datei im Review ist, sind Kommentare an Knoten-IDs gebunden. Das Entfernen eines Frames und das Zeichnen eines neuen verwaist jeden Kommentar darauf. Während einer Review-Runde bearbeite vorhandene Knoten direkt (siehe Der Review-Loop).
Ein schlechter Wert zerstört den gesamten Build
Jeder Fehler in dieser Datei hat die gleiche Form: ein einzelner Wert, den die API ablehnt, der mitten im Zeichnen ausgelöst wird und eine halb gerenderte Seite hinterlässt, die wie ein Layout-Problem aussieht. Typische Übeltäter: eine negative Breite, eine feste Texthöhe von 10px, eine nicht geladene Schriftstärke.
Klemme an der Grenze, anstatt der Arithmetik zu vertrauen.
function W1(v, min){ const n = Number(v); return (isFinite(n) && n > 0) ? n : (min || 1); }
Leite jede Breite, Höhe und Textbreite durch sie. Eine Klemmfunktion kostet eine falsch aussehende Box; ein Throw kostet die Seite.
Die Arithmetik, die beißt, ist eine feste Subtraktion, die nur bei einer Breite gilt:
const FEED = DW - PAD*2 - 400; // 840 bei 1440. MINUS 50 bei 390.
Jede Konstante, die von der Canvas-Breite subtrahiert wird, braucht eine Breakpoint-Wache, sonst wird sie bei der kleinen negativ.
Symptom zum Erkennen: Die breiten Frames sehen fertig aus, die schmalen fehlen oder sind halb gezeichnet und überlappen etwas anderes. Das ist kein Positionierungsfehler. Lies den Fehler-Toast – er nennt die Eigenschaft und den Knoten.
Zeichne niemals am Ursprung
Ein Plugin, das bei (0, 0) beginnt, landet auf dem, was bereits da ist, und ein Bereiniger, der nur seine eigenen Frames entfernt, wird dich nicht retten: Zwei Plugins, die sich eine Seite teilen, glauben beide, das Canvas sei leer.
Miss zuerst, dann beginne klar:
let startY = 0;
figma.currentPage.children.forEach(n => { startY = Math.max(startY, n.y + n.height); });
if (figma.currentPage.children.length) startY += 600;
Beschrifte jede Zeile auf dem Canvas und stelle sicher, dass der Bereiniger alles abdeckt, was der Build ausgibt – sowohl Frames als auch Labels –, sonst bleiben beim erneuten Ausführen verwaiste Texte zurück.
Textgrößenanpassung, in dieser exakten Reihenfolge
Der teuerste Fehler. Figma berechnet die Höhe eines Textknotens neu, wenn sich sein Inhalt ändert, nicht wenn du ihn in der Größe änderst.
const str = String(s);
const t = figma.createText();
t.fontName = { family: "Inter", style: weight };
t.characters = str; // 1. ZUERST
t.fontSize = size; // 2. dann Stile
t.lineHeight = { unit: "PERCENT", value: 145 };
if (width) {
t.textAutoResize = "HEIGHT"; // 3. bleibt NICHT auf einem leeren Knoten
t.resize(width, t.height); // niemals eine buchstäbliche Höhe
t.characters = str; // 4. Erzwinge eine erneute Messung bei der neuen Breite
} else {
t.textAutoResize = "WIDTH_AND_HEIGHT";
}
textAutoResize = "HEIGHT"gesetzt bevor der Knoten Zeichen hat, wird verworfen. Eine Neuordnung, die den Inhalt an das Ende setzt, ohne die Eigenschaft zu verschieben, macht es schlimmer.resize(w, 10)fixiert die Höhe für immer auf 10. Übergibt.height.- Die erneute Zuweisung desselben Strings ist die doppelte Absicherung für die erneute Messung.
Symptom: Jeder Text mit einer expliziten Breite ist ~10px hoch, jeder Text ohne ist korrekt, und jeder Frame, dessen Höhe nur vom Text kommt, fällt auf seine Polsterung zusammen.
width bedeutet feste Breite – welche Achse ist nicht offensichtlich
if (opts.horizontal) {
f.primaryAxisSizingMode = width ? "FIXED" : "AUTO"; // Breite IST die primäre Achse
f.counterAxisSizingMode = "AUTO";
} else {
f.counterAxisSizingMode = width ? "FIXED" : "AUTO"; // Breite ist die sekundäre Achse
f.primaryAxisSizingMode = "AUTO";
}
Das Setzen von counterAxisSizingMode auf einem horizontalen Frame fixiert seine Höhe und lässt die Breite umschließen. Jede so gebaute Zeile hat keine zusätzliche Breite, also kann kein Kind darin wachsen.
Ändere die Größe, bevor du die Größenmodi setzt, sonst kämpft die explizite Größe mit dem Umschließen.
Eine Spalte, die eine Zeile füllt
function growCol(n) { n.layoutGrow = 1; n.counterAxisSizingMode = "FIXED"; return n; }
Ein vertikaler Frame, der mit width: null erstellt wurde, umschließt. Gib ihm layoutGrow = 1 und STRETCH-Kinder, und du hast eine zirkuläre Einschränkung: Der Elternteil passt sich an die Kinder an, die Kinder passen sich an den Elternteil an. Figma löst es auf dem Minimum auf.
Symptom: Karten ~45px breit, Body-Kopien mit einem Zeichen pro Zeile, Textknoten melden 1 x 988. Seitenrahmen dreimal höher als sie sein sollten, vollständig aufgrund falschen vertikalen Raums.
Gib Zeilen lieber eine explizite Breite, als dich auf STRETCH zur Auflösung zu verlassen.
Messe, schau nicht
Geometrie beantwortet die Frage schneller als ein Screenshot und deckt alle oben genannten Fehler auf. Ziehe get_metadata, dann:
- Höhen ≤ 12px → zusammengefallener Text oder Frames
- Breiten von 1 → die zirkuläre Einschränkung oben
- Eine Seite weit höher als ihr HTML-Äquivalent → schmales Umbrechen, nicht Inhalt
- Eine Kopie jedes Frames, wenn du zwei erwartet hast → der Durchlauf fand nie statt
Überprüfe die Summen: Die y-Bereiche eines Blocks sollten seine Seite lückenlos kacheln, und der letzte Block sollte exakt auf der Seitenhöhe enden.
Simulieren des Plugins lokal
tools/figma_stub.js in diesem Skill ist ein gebrauchsfertiger strikter Stub; node tools/test_figma_stub.js beweist, dass er immer noch dort wirft, wo Figma wirft.
Ein Stub des figma-Globals fängt Throws, null-Anhängungen und strukturelle Fehler ab, bevor ein Roundtrip stattfindet. Er kann keine Textmetriken emulieren, also beweist er, dass der Code läuft, nie, dass das Layout richtig ist.
Mache den Stub so streng wie die echte API, sonst wird er Builds bestehen lassen, die in Figma sterben. Jeder dieser Punkte ist ein Fehler, der das Canvas erreichte, weil der Stub nachsichtig war:
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"); ... }
Gib dem Stub currentPage.children und füttere ihn mit den Frames, die die echte Seite bereits enthält – so zeigt sich eine Ursprungskollision lokal.
Um die Positionierung zu überprüfen, muss der Stub auch Auto-Layout-Höhen berechnen: Summiere die Kinder plus itemSpacing und Polsterung für einen VERTICAL-Frame, nimm das Maximum für einen HORIZONTALEN und gib den festen Wert zurück, wenn primaryAxisSizingMode === "FIXED". Grob ist ausreichend, um Zeilenüberlappung zu erkennen.
Führe jeden Dispatch-Pfad, jeden Breakpoint und jedes Schrift-Setup aus, nicht nur den glücklichen:
Nur Inter · Inter + Mono · alle drei · Nur Inter Regular
Desktop · Mobil Dunkel · Hell
Schriftarten: Lade, was du anfordern wirst, und löse auf, was du bekommst
Das Zuweisen einer nicht geladenen Schriftart wirft einen Fehler. Zwei Fallen dahinter:
- Du musst jedes Gewicht vorladen, das der Code anfordert. Das Laden von Regular, Medium und Bold und dann die Anforderung von Semi Bold ist ein Absturz, der auf die eine Karte wartet, die es verwendet.
- Der Stilname unterscheidet sich pro Schriftfamilie. Inter sagt
"Semi Bold"; JetBrains Mono sagt"SemiBold". Das Anfordern des richtigen Gewichts mit der falschen Schreibweise wirft immer noch einen Fehler.
Prüfe jedes Gewicht separat, protokolliere, was tatsächlich geladen wurde, und löse jede Anfrage über eine Fallback-Kette auf, anstatt anzunehmen:
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" };
}
Lade niemals strikt im Einstiegspunkt mit Promise.all vor – ein fehlendes Gewicht dort tötet einen Build, den der Resolver behandelt hätte. Melde die Schriftfamilien, auf die du aufgelöst hast, im abschließenden Toast.
Ein Plugin, eine Aufgabe
Widerstehe einem Dispatch-Switch innerhalb eines einzelnen Plugins – nach Editor-Typ, Seitenname oder Auswahl. Ein Seitenname-Switch bedeutet, dass der Benutzer ein Ritual durchführen muss, bevor das Ding funktioniert, und wenn es stillschweigend das Falsche tut, gibt es nichts zu lesen.
Zwei Manifeste mit unterschiedlichen id- und name-Werten kosten nichts und beseitigen die gesamte Klasse von Problemen. Figma verwendet auch eine gecachte Registrierung wieder, wenn ein erneut importiertes Manifest eine id trägt, die es bereits kennt. Das Ändern der id ist der Weg, um eine saubere Ladung zu erzwingen.
Kommentare wieder auslesen
Das MCP bietet get_metadata, get_design_context, get_screenshot, get_figjam, use_figma – keines davon liest Kommentare, und auch die Plugin-API hat keinen Zugriff auf Kommentare. Kommentare leben in der REST-API.
Verwende tools/figma_comments.py in diesem Skill-Ordner (Python 3, Standardbibliothek):
T=~/.claude/skills/figma-build/tools/figma_comments.py
python3 $T check <file-url> # OK: Token liest Kommentare (N auf Datei KEY)
python3 $T fetch <file-url> # offene Threads als JSON, Ledger und erledigte herausgefiltert
python3 $T mark <file-url> ID... --change "was sich geändert hat"
python3 $T reply <file-url> ROOT_ID "Erledigt: Logo 20% größer" # benötigt den Schreibbereich
Es liest den Token aus ~/.figma-token und gibt ihn niemals aus. Führe es aus dem Arbeitsordner aus, damit das Ledger dort landet, oder setze FIGMA_LEDGER. Es gibt keinen Löschbefehl. Wenn es einen SSL-Zertifikatsfehler meldet, hat das Python keine Root-Zertifikate: Führe /usr/bin/python3 stattdessen aus, oder pip install certifi.
Bereich: Dateikommentare → Lesen. Speichere den Token in einer Datei (~/.figma-token, chmod 600), niemals in der Konversation. Wenn die Person keinen Shell-Befehl ausführen kann, schreibe die Datei mit einem Platzhalter und open -a TextEdit zum Einfügen.
/v1/me gibt 403 auf einem kommentar-beschränkten Token zurück. Das ist erwartet, kein defekter Token. /v1/files/:key/nodes gibt ebenfalls 403 ohne file_content:read.
Jeder Kommentar trägt client_meta:
{ "node_id": "2:95", "node_offset": { "x": 279, "y": 1257 } }
Ordne es einem Block zu, indem du get_metadata ziehst und den untergeordneten Frame findest, dessen y-Bereich den Offset enthält. Threads kommen flach zurück – gruppiere nach parent_id, und Antworten tragen oft die eigentliche Anweisung, während die Wurzel nur ein Zeiger ist.
Kommentare sind pro Datei. Das Verschieben von Inhalten in eine neue Datei lässt sie zurück.
Der Review-Loop, wenn du gebeten wirst, „meine Kommentare zu lesen”
- Hole mit
figma_comments.py fetch, das Kommentare in Threads gruppiert. Überspringe erledigte (resolved_atgesetzt) und jede ID, die bereits im bearbeiteten Ledger (unten) ist. Alte Kommentare bleiben in der Datei, bis sie erledigt werden. Ohne diese beiden Filter wendet jede spätere Review und jede spätere Konversation die Rückmeldungen der vorherigen Runde erneut an. - Ordne jeden Thread seinem Frame durch
client_metazu, wie oben. - Bestätige vor dem Zeichnen: Liste jeden Kommentar, den Frame, dem er zugeordnet ist, und die geplante Änderung in einer kurzen Tabelle auf. Ein Kommentar, der auf leerem Canvas gepinnt ist oder mehrdeutig ist, erhält eine Frage anstelle einer Vermutung.
- Wende die Änderungen vor Ort an: Ändere die Eigenschaften der vorhandenen Knoten, behalte ihre IDs. Entferne niemals einen kommentierten Frame und zeichne ihn neu, da dies seine Kommentare verwaist. Über das MCP (
use_figma) wird das Canvas direkt aktualisiert. Über ein Entwicklungs-Plugin bearbeite seinen Code auf der Festplatte, erhöhe den Build-Stempel und bitte die Person, das Plugin erneut auszuführen. - Überprüfe mit
get_metadataund einem Screenshot (siehe Messe, schau nicht). Auf dem Plugin-Weg überprüfe, ob der abschließende Toast den neuen Stempel zeigt, bevor du dem Canvas vertraust. - Berichte Kommentar für Kommentar, was sich geändert hat. Wenn der Token den Schreibbereich hat und die Person es möchte, antworte in jedem Thread.
- Protokolliere jede bearbeitete Kommentar-ID (Thread-Wurzeln und Antworten) mit
figma_comments.py mark, bevor die Runde endet, einschließlich derer, die du nur mit einer Frage beantwortet hast. - Lösche niemals Kommentare, selbst wenn du gebeten wirst, aufzuräumen. Sie sind die Review-Aufzeichnung. Alte Rückmeldungen bleiben durch das Ledger außen vor, nicht durch Löschen. Wenn eine Runde in Figma gelöscht werden muss, erledigt die Person die Kommentare.
Das bearbeitete Ledger. Eine JSON-Datei neben der Arbeit: figma/comments-handled.json in einem Kampagnenordner, sonst figma-comments-handled.json im Arbeitsverzeichnis. Lies es zu Beginn jeder Review, auch in einer neuen Konversation.
{ "<file_key>": { "<comment_id>": { "handled_at": "2026-11-03T14:05:00Z", "change": "Logo 20% größer auf 1080x1920" } } }
Es gibt keinen API-Endpunkt zum Erledigen eines Kommentars, also bleibt das Erledigen bei der Person.
Wann um einen erneuten Import bitten (nur Plugin-Weg): wenn der Toast einen veralteten Stempel zeigt oder „replaced 0”, während Frames sichtbar sind. Bitte sie, das Plugin zu entfernen, sein manifest.json erneut zu importieren und den Tab neu zu öffnen. Wenn das fehlschlägt, ändere die Manifest-id (siehe Ein Plugin, eine Aufgabe).
Screenshots in großem Maßstab
Ein Screenshot pro Seite plus lokale Zuschnitte ist besser als ein Aufruf pro Block. Die Block-y/Höhe kommt von get_metadata, maxDimension auf oder über der Frame-Höhe ergibt 1:1, und die Zuschnitte passen genau.