Communitygithub.com

l4ur4oliveira/astroville

Ce document compile les leçons apprises lors de la construction du Design System AstroVille sur Penpot via le serveur MCP, afin d’éviter de redécouvrir ces bizarreries de Penpot dans de futurs projets.

Qu'est-ce que astroville ?

Ce guide de résolution de problèmes pour Penpot MCP rassemble les solutions à des problèmes récurrents rencontrés lors de la création du Design System AstroVille. Il couvre des bugs non documentés comme le positionnement négatif des enfants après un groupement, les dimensions gigantesques des textes avec `lineHeight` en pixels, ou l'export PNG qui ne rend rien. Pour chaque problème, une cause racine et un correctif étape par étape sont fournis, avec des exemples de code JavaScript. Ce guide aide les développeurs et utilisateurs d'agents IA à anticiper et résoudre rapidement les problèmes de positionnement, de rendu et d'export dans Penpot, quel que soit leur niveau d'expérience avec l'outil. Les plateformes supportées incluent l'API MCP de Penpot et les agents comme Claude Code via l'outil `penpot`.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/l4ur4oliveira/astroville/tree/HEAD/.agents/skills/penpot-problem-solving

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Guide de résolution de problèmes Penpot MCP

Objectif

Ce document compile les leçons apprises lors de la construction du Design System AstroVille sur Penpot via le serveur MCP. L’objectif est d’éviter de redécouvrir ces bizarreries de Penpot dans de futurs projets. Beaucoup de ces comportements ne sont pas documentés et n’apparaissent qu’au moment de l’export ou du groupement des éléments.

La leçon la plus importante : toujours encapsuler le contenu d’une section dans un Groupe, puis positionner tous les éléments à l’intérieur du Groupe en utilisant des coordonnées (x, y) simples relatives à l’origine du groupe. Cela réduit considérablement les erreurs de positionnement.


TL;DR — La règle « Groupe d’abord »

Pour chaque section de premier niveau d’un design Penpot (par exemple, un planche représentant « Couleurs », « Typographie », « Espacement ») :

  1. Créez la planche.
  2. Encapsulez tous ses enfants dans un Groupe (Penpot le crée automatiquement lorsque vous appelez penpot.group([...enfants])).
  3. Positionnez le Groupe lui-même en haut à gauche de votre zone de contenu, par ex. penpotUtils.setParentXY(groupe, 64, 40).
  4. À l’intérieur du Groupe, toutes les positions des enfants sont des coordonnées simples (0, 0) relatives à l’origine du groupe.
  5. Positionnez toujours les enfants d’abord en coordonnées relatives (0, 0), PUIS groupez-les, et non l’inverse.

Ce modèle est ce qui rend le positionnement prévisible et l’export correct.


Problème 1 — Les enfants d’une carte obtiennent des parentX / parentY négatifs après le groupement

Symptôme

Après avoir appelé penpot.group([...enfants]) pour encapsuler le contenu d’une section, les enfants imbriqués à l’intérieur des cartes se retrouvent avec des valeurs parentX / parentY négatives. Par exemple, un Rectangle censé être à (0, 0) par rapport à sa carte se retrouve à (-64, -210) — dessiné en dehors des limites de la carte.

Visuellement, les fonds des cartes (les échantillons de couleur) semblent vides car le rectangle de remplissage réel est positionné à l’extérieur.

Cause racine

Lorsque vous groupez des formes qui étaient déjà disposées en positions absolues sur une planche parente, Penpot calcule la boîte englobante du groupe. Le groupe lui-même est positionné à (minX, minY) de ses membres. Ensuite, pour chaque enfant, son parentX / parentY est recalculé par rapport à la nouvelle origine du groupe.

Si la position absolue d’un enfant était, disons, (64, 218) et que le groupe se retrouve à (64, 218), le parentX / parentY de l’enfant devient (0, 0). Mais si la carte contenant l’enfant était à (64, 218) et que le groupe se retrouve à, disons, (64, 40), alors tout ce qui se trouve à l’intérieur de la carte obtient un parentY négatif car la carte est « plus haute » que l’origine du groupe.

Correctif

Positionnez toujours les enfants en coordonnées relatives à l’origine future du groupe (0, 0) AVANT d’appeler penpot.group(). Après le groupement, l’origine du groupe devient (0, 0) pour ses enfants.

Si vous avez déjà groupé et que les enfants sont mal alignés, la récupération est :

  1. penpot.ungroup(groupe) — repositionnez à plat.
  2. Soustrayez le décalage prévu du groupe de la position y (ou x) de chaque enfant.
  3. penpot.group(enfants) à nouveau.
  4. penpotUtils.setParentXY(groupe, parentX souhaité, parentY souhaité).

Concrètement (le modèle qui a fonctionné) :

// 1. Sauvegardez les positions absolues originales que vous souhaitez conserver.
const positionsOriginales = enfants.map(e => ({ e, x: e.x, y: e.y }));

// 2. Décalez chaque enfant de -decalageGroupe (pour qu'ils soient relatifs à l'origine du groupe).
for (const enfant of enfants) {
  enfant.y -= DECALAGE_GROUPE_Y;   // par ex. 40
}

// 3. Groupez-les.
const groupe = penpot.group(enfants);

// 4. Positionnez le groupe lui-même.
if (groupe) {
  penpotUtils.setParentXY(groupe, groupe.parentX, DECALAGE_GROUPE_Y);
}

Problème 2 — Les formes de texte avec lineHeight fixe (valeur en pixels) s’affichent avec une hauteur énorme

Symptôme

Une forme de texte avec fontSize: 48 et lineHeight: 56 (une valeur en pixels) se retrouve avec bounds.height: 2689 (ou une valeur absurde similaire). Le texte se répartit verticalement — le texte réellement rendu apparaît bien en dessous de l’endroit où vous l’avez placé (souvent à y: 1313 en termes absolus), rendant le texte invisible.

Cause racine

lineHeight de Penpot accepte un multiplicateur numérique sous forme de chaîne comme "1.2", mais rejette certaines valeurs comme "56" ou toute valeur non numérique autre qu’un multiplicateur. Lorsqu’une valeur invalide est passée, Penpot utilise silencieusement une hauteur de ligne par défaut qui peut être énorme, provoquant un retour à la ligne. L’export SVG confirme le bogue — l’attribut y de l’élément <text> rapporte une valeur énorme comme 1386.44.

De plus, même lorsqu’une lineHeight en pixels apparemment valide est acceptée, le calcul des limites pour un texte auto-width peut produire des valeurs étranges comme width: 338 mais height: 2689, car le moteur de texte effectue un retour à la ligne à l’intérieur des limites que le texte pense avoir à sa création.

Correctif

Utilisez toujours lineHeight: "1.2" (un multiplicateur sous forme de chaîne) plutôt que des valeurs en pixels. La forme de texte aura alors des limites correctes et prévisibles :

const t = penpot.createText("ASTROGENESIS");
parent.appendChild(t);
t.resize(800, 80);                  // redimensionnez AVANT de définir fontSize
t.x = 64;
t.y = 100;
t.fontFamily = "Space Grotesk";
t.fontWeight = "700";
t.fontSize = 48;
t.lineHeight = "1.2";               // ← utilisez "1.2", pas "56"
t.fills = [{ fillColor: "#F8FAFC", fillOpacity: 1 }];

Cela fonctionne que le fontSize soit défini avant ou après l’ajout, tant que resize() est appelé AVANT fontSize et que lineHeight est une chaîne multiplicateur.


Problème 3 — Les changements de fontSize après l’ajout donnent des dimensions 1×1 si resize() n’est pas appelé d’abord

Symptôme

Un élément de texte créé avec penpot.createText(), ajouté à un parent, et ensuite doté de fontSize: 32 se retrouve avec width: 1, height: 1 initialement. Après un certain délai, les limites se rétablissent, mais le texte peut s’afficher à des positions inattendues.

Cause racine

Penpot mesure les dimensions du texte en utilisant les limites courantes. Si le texte est créé avec des dimensions 1×1 au moment où le moteur de police mesure, il le traite comme contraint à 1 pixel de large et effectue un retour à la ligne agressif.

Correctif

Appelez toujours resize(largeur, hauteur) AVANT de définir fontSize. Cela donne au texte une boîte englobante connue. L’assistant le plus simple :

function creerTexte(parent, chaine, opts) {
  const t = penpot.createText(chaine);
  parent.appendChild(t);
  t.resize(opts.l || 400, opts.h || 60);  // redimensionnez d'abord
  t.x = opts.x;
  t.y = opts.y;
  t.fontFamily = opts.fontFamily || "Inter Tight";
  t.fontWeight = opts.fontWeight || "400";
  t.fontSize = opts.fontSize || 14;
  t.lineHeight = opts.lineHeight || "1.2";  // multiplicateur, PAS des pixels
  t.fills = [{ fillColor: opts.couleur || "#F8FAFC", fillOpacity: 1 }];
  if (opts.transform === "majuscules") t.textTransform = "uppercase";
  return t;
}

Problème 4 — L’espacement des lettres avec des unités em est rejeté

Symptôme

Définir text.letterSpacing = "0.08em" échoue avec Valeur non valide : 0.08em. Code : :letterSpacing.

Correctif

letterSpacing de Penpot n’accepte que des valeurs numériques (interprétées comme 1/100 em, ou similaire), pas des chaînes avec suffixe em. Utilisez "1" (ou 0, "2", etc.) au lieu de "0.08em". Pour un contrôle plus fin, utilisez un petit nombre comme "1" ou "2".


Problème 5 — penpot.generateMarkup() renvoie un contenu que l’export PNG ne rend pas

Symptôme

Vous créez des formes, le balisage SVG les montre correctement (<rect>, <text>, <stop> de gradient, etc.), mais export_shape retourne un PNG vierge ou dépourvu du contenu attendu. Il n’y a aucun message d’erreur.

Cause racine

Deux causes principales observées :

  1. Planches imbriquées avec clipContent: true et petites dimensions peuvent rogner la zone visible. Penpot exporte en utilisant la boîte englobante, mais le contenu intérieur dépasse.
  2. Encapsulations de groupes avec contenu décalé (Problème 1 ci-dessus) font que l’export de Penpot utilise des limites de texte obsolètes. Le balisage SVG peut montrer les éléments, mais l’export ne respecte pas les décalages.

Correctif

  • Faites confiance au balisage SVG (penpot.generateMarkup) comme vérité première lorsque quelque chose ne s’affiche pas.
  • Après avoir corrigé le Problème 1 (enfants aux positions relatives correctes), les exports fonctionnent correctement.
  • Vérifiez en exportant d’abord de petites planches individuelles pour isoler celle qui échoue.

Problème 6 — Les enfants d’une planche de section disparaissent après le groupement

Symptôme

Après le groupement, les échantillons / cartes / étiquettes de la section disparaissent de l’export visible, mais ils existent toujours dans la structure de données. Les conteneurs des cartes apparaissent (leurs bordures, remplissages) mais leur contenu (enfants Rectangle, Texte) est dessiné hors de la carte.

Cause racine

C’est la même chose que le Problème 1 — les enfants des cartes ont des parentX / parentY négatifs et sont dessinés en dehors des limites de leur carte.

Correctif

Après le groupement, parcourez les cartes et réinitialisez explicitement la position de chaque enfant imbriqué à son emplacement prévu par rapport à sa carte. Par exemple, dans une carte de couleur avec un échantillon Rectangle à (0, 0) et trois étiquettes de texte à (16, 132), (16, 154), (16, 174) :

const cartes = groupe.enfants.filter(e => e.nom && e.nom.startsWith("Carte Couleur"));
for (const carte of cartes) {
  const ch = carte.enfants;
  if (ch.length < 4) continue;
  penpotUtils.setParentXY(ch[0], 0,   0);    // Rectangle en haut à gauche de la carte
  penpotUtils.setParentXY(ch[1], 16,  132);  // étiquette
  penpotUtils.setParentXY(ch[2], 16,  154);  // nom du jeton
  penpotUtils.setParentXY(ch[3], 16,  174);  // valeur hexadécimale
}

Problème 7 — La racine de la page (page.root) a des dimensions fixes minuscules, donc les planches larges peuvent casser les exports

Symptôme

La planche racine de la page Penpot est toujours de taille 0.01 × 0.01 — ses dimensions ne peuvent pas être redimensionnées. Tout ce que vous y ajoutez vit en coordonnées absolues en dehors de la boîte englobante de la racine.

Cause racine

Penpot traite la racine de la page comme un espace réservé ; tout le contenu réel vit dans des planches nommées placées en positions absolues sur la page.

Correctif

C’est acceptable si vous gardez les choses dans des planches nommées avec leurs propres coordonnées absolues (comme vous le faites normalement). N’essayez pas d’utiliser page.root.resize(). Si vous avez besoin d’un conteneur maître, placez les planches enfants directement sur page.root plutôt que de les imbriquer dans une planche maîtresse.


Problème 8 — Ombres : color doit être { color: "#hex", opacity: 0..1 }, pas { r, g, b, a }

Symptôme

Définir une ombre comme :

forme.ombres = [{
  color: { r: 139/255, g: 92/255, b: 246/255, a: 0.25 },
  offsetX: 0, offsetY: 0, blur: 24, spread: 0,
  style: "drop-shadow", visible: true
}];

Se rend avec du noir et une faible opacité au lieu de violet. La propriété .shadows de la forme se lit comme :

{ color: { color: "#000000", opacity: 0.2 }, ... }

Cause racine

Shadow.color est un objet Color de Penpot ayant la forme { color: "#hex", opacity: 0..1 } — pas un objet {r, g, b, a}. La forme erronée est silencieusement ignorée et Penpot revient à une ombre portée noire par défaut.

Correctif

Utilisez la forme d’objet Color avec une chaîne hexadécimale et un champ opacity séparé :

forme.ombres = [{
  color: { color: "#8B5CF6", opacity: 0.25 },
  offsetX: 0, offsetY: 0, blur: 24, spread: 0,
  style: "drop-shadow", visible: true
}];

Ou utilisez penpot.shapesColors([forme]) pour obtenir une instance de Color et inspecter ses champs avant de l’appliquer.


Problème 9 — startX / startY / endX / endY du gradient sont normalisés, pas en pixels

Symptôme

Définir un gradient avec des coordonnées en pixels :

forme.remplissages = [{
  fillColorGradient: {
    type: "linear",
    startX: 0, startY: 0, endX: 320, endY: 160,
    stops: [...]
  },
  fillOpacity: 1
}];

Se relit avec endX: ~1.0, endY: ~1.0 — Penpot a silencieusement normalisé le vecteur. Le champ width conserve l’échelle d’origine, mais le vecteur de direction est maintenant dans l’espace [0, 1].

Cause racine

Penpot stocke la direction du gradient comme un vecteur normalisé entre 0 et 1. Les valeurs en pixels sont divisées par width. Si le vecteur résultant n’est pas strictement dans [0, 1] (par ex. endX: 320 / 320 = 1.0 exactement, ou endY: 160 / 320 = 0.5), Penpot semble l’écrêter et l’arrondir.

Dans AstroVille, l’utilisateur a fait glisser le second point de couleur dans l’interface utilisateur de Penpot pour le placer dans le coin inférieur droit du rectangle. Les valeurs obtenues étaient endX: ~1.0, endY: ~1.0. Le positionnement pixel-parfait du gradient n’est pas exposé comme une propriété modifiable — l’interface utilisateur est la source de vérité pour les positions des poignées de gradient.

Correctif

  • Pour un gradient horizontal (de gauche à droite) : endX: 1, endY: 0
  • Pour un gradient diagonal (haut-gauche → bas-droite) : endX: 1, endY: 1
  • Pour un gradient vertical (haut → bas) : endX: 0, endY: 1
  • Pour des positions d’arrêts précises le long de la direction, utilisez offset (0–1) par arrêt.
  • Si les positions exactes des poignées de gradient sont importantes, faites-les dans l’interface utilisateur de Penpot et relisez-les pour confirmer.

Modèle recommandé : Le modèle de page « Fondations » AstroVille

La structure qui a fonctionné pour la page « 00 — Design System » d’AstroVille :

page.root
├── "00 — Design System / En-tête" (planche, 0, 0, 1440×120)
│   └── Groupe à (64, 40)
│       ├── "Design System"  texte à (0, 0)   → rendu à (64, 40)
│       └── "AstroVille — Fondations" texte à (0, 52) → (64, 92)
│
├── "01 — Couleurs" (planche, 0, 200, 1440×1460)
│   └── Groupe à (64, 40)
│       ├── Textes Titre + Description à (0, 0), (0, 48)
│       ├── Étiquette "Arrière-plan" à (0, 100)
│       ├── 3 Cartes Couleur en ligne à (0, 178), (264, 178), (528, 178)
│       │   Chaque carte a ses propres enfants à (0, 0), (16, 132), (16, 154), (16, 174)
│       └── ... 3 autres groupes (Marque, Feedback, Texte) à (0, 498), (0, 818), (0, 1138)
│
└── ... autres sections avec la même structure

Décalages y des sections (relatifs à (0, 0) du groupe) :

Type de sectionDisposition
En-têteTitre (0, 0), Sous-titre (0, 52)
Bloc de titreTitre (0, 0), Description (0, 48)
En-tête de groupeÉtiquette (0, 100), Description (0, 130)
Ligne de cartes3 cartes à (0, 178), (264, 178), (528, 178)
Ligne suivante320 px en dessous de la précédente : (0, 498), (0, 818), (0, 1138)

Assistant éprouvé pour créer des planches et des textes :

function creerTexte(parent, chaine, opts) {
  const t = penpot.createText(chaine);
  parent.appendChild(t);
  t.resize(opts.l || 400, opts.h || 60);
  t.x = opts.x;
  t.y = opts.y;
  t.fontFamily = opts.fontFamily || "Inter Tight";
  t.fontWeight = opts.fontWeight || "400";
  t.fontSize = opts.fontSize || 14;
  t.lineHeight = opts.lineHeight || "1.2";
  t.fills = [{ fillColor: opts.couleur || "#F8FAFC", fillOpacity: 1 }];
  if (opts.transform === "majuscules") t.textTransform = "uppercase";
  return t;
}

Résumé des causes racines et des correctifs

ProblèmeCause racineCorrectif
Enfants de carte avec parentX/parentY négatifsPenpot repositionne les enfants par rapport à l’origine du groupeDécaler les enfants à (0, 0) relatif avant de grouper
Texte avec hauteur énormelineHeight en pixels rejeté ; valeur par défaut énorme utiliséeUtiliser un multiplicateur sous forme de chaîne comme "1.2"
Texte avec dimensions 1×1fontSize défini avant resize()Appeler resize() avant fontSize
letterSpacing rejetéL’unité em n’est pas prise en chargeUtiliser une valeur numérique simple
Export PNG videContenu rogné ou décalages obsolètesVérifier avec generateMarkup ; corriger les décalages
Enfants disparaissent après groupementMême cause que le Problème 1Réinitialiser les positions relatives des enfants de carte
La racine de la page ne peut pas être redimensionnéeLa racine de la page est un espace réservé fixePlacer le contenu dans des planches nommées
Ombre rendue en noirFormat de couleur incorrect pour shadow.colorUtiliser { color: "#hex", opacity: n }
Positions de gradient normaliséesPenpot normalise les vecteurs de gradient en [0, 1]Utiliser endX: 1, endY: 1 pour un diagonal complet

Comment utiliser ce guide (pour les agents IA)

Lorsqu’un agent IA génère du code de design Penpot (via l’outil penpot), référez-vous à ce guide dans les cas suivants :

  1. Erreur de positionnement — Les éléments sont décalés ou en dehors des cartes. → Voir Problème 1.
  2. Texte manquant ou en hauteur — Le texte a des dimensions étranges ou invisibles. → Voir Problèmes 2–3.
  3. Export échoue — L’export PNG ne rend pas le contenu attendu. → Voir Problème 5.
  4. Ombres ou gradients incorrects — Les couleurs ou directions sont erronées. → Voir Problèmes 8–9.
  5. Règle générale — Toujours encapsuler le contenu dans un groupe, puis décaler les enfants en coordonnées relatives.

Pour les nouvelles sections, utilisez le Modèle de page AstroVille ci-dessus avec les décalages fournis.


Documenté à partir de l’expérience pratique de construction du Design System AstroVille sur Penpot via MCP.

Skills associés