From 59cad6735a42e9776cd78a3f20ca88b87a12688c Mon Sep 17 00:00:00 2001 From: Nox Date: Sat, 28 Feb 2026 14:19:26 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20param=C3=A8tres=20create=5Ftext=20(text/?= =?UTF-8?q?textAlign),=20template=20Penpot=20corrig=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MEMORY.md | 15 +++- penpot-make-poster.mjs | 194 ++++++++++++++++------------------------- penpot-template.mjs | 194 ++++++++++++++++------------------------- 3 files changed, 162 insertions(+), 241 deletions(-) diff --git a/MEMORY.md b/MEMORY.md index be0e82a..fccace2 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -135,16 +135,25 @@ **5. `list_teams` retourne du texte** ("Found 1 teams"), pas du JSON -**6. Workflow création fichier** : +**6. `create_text` — paramètres corrects** : +- `text` (PAS `content`) → le contenu du texte +- `textAlign` (PAS `align`) → "left", "center", "right", "justify" +- `fontWeight` → string "100"–"900" ou "bold" +- `fillColor` → couleur HEX +- ⚠️ `parentId` **NON SUPPORTÉ** pour create_text — les textes vont toujours au root frame +- Width/height auto-calculés (text.length × fontSize × 0.6) — ne pas les spécifier + +**7. Workflow création fichier** : ``` get_profile → defaultProjectId create_file { projectId, name } → fileId list_pages { fileId } → pageId (array[0].id) create_frame { fileId, pageId, name, x, y, width, height, fillColor } → frameId -create_rectangle/text { fileId, pageId, parentId: frameId, ... } +create_rectangle { fileId, pageId, parentId: frameId, ... } ← parentId OK pour rect +create_text { fileId, pageId, text, x, y, textAlign, ... } ← PAS de parentId ``` -**7. Coordonnées shapes dans un frame** : absolues (pas relatives au frame) +**8. Coordonnées** : absolues sur la page. Les textes se placent dans le root frame automatiquement. Pour un poster : mettre frame à (0,0) et textes aux bonnes coords absolues. ### Pour générer une image finale - `export_shape` non dispo → utiliser **HTML/CSS → Playwright screenshot** diff --git a/penpot-make-poster.mjs b/penpot-make-poster.mjs index b2bcc3d..a49c6e9 100644 --- a/penpot-make-poster.mjs +++ b/penpot-make-poster.mjs @@ -1,6 +1,10 @@ /** - * Script tout-en-un : redémarre le MCP + crée l'affiche SOLDES - * Paramètres MCP = camelCase ! + * Affiche SOLDES -50% — version corrigée + * Leçons apprises : + * - create_text : paramètre `text` (pas `content`), `textAlign` (pas `align`) + * - create_text : parentId NON supporté → textes à coords absolues + * - create_rectangle : parentId OK + * - Frame à (0,0) → coordonnées absolues = relatives au frame */ import https from 'https'; import http from 'http'; @@ -17,8 +21,7 @@ function portainerReq(path, method = 'GET') { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve(res.statusCode); } }); }); - req.on('error', reject); - req.end(); + req.on('error', reject); req.end(); }); } @@ -56,21 +59,14 @@ function postMCP(body) { if (line.startsWith('data:')) { const raw = line.slice(5).trim(); if (!raw || raw === '[DONE]') continue; - try { - const j = JSON.parse(raw); - if (j.result !== undefined) result = j.result; - if (j.error) error = j.error; - } catch {} + try { const j = JSON.parse(raw); if (j.result !== undefined) result = j.result; if (j.error) error = j.error; } catch {} } } if (error) reject(new Error(JSON.stringify(error))); else resolve(result); } else { - try { - const j = JSON.parse(data); - if (j.error) reject(new Error(JSON.stringify(j.error))); - else resolve(j.result ?? j); - } catch { resolve(data || null); } + try { const j = JSON.parse(data); if (j.error) reject(new Error(JSON.stringify(j.error))); else resolve(j.result ?? j); } + catch { resolve(data || null); } } }); }); @@ -81,156 +77,116 @@ function postMCP(body) { async function initMCP() { await postMCP({ jsonrpc: '2.0', id: msgId++, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'nox', version: '1.0' } } }); await postMCP({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }).catch(() => {}); - console.log('✅ MCP Session:', sessionId); + console.log('✅ Session:', sessionId); } async function callTool(name, args = {}) { const result = await postMCP({ jsonrpc: '2.0', id: msgId++, method: 'tools/call', params: { name, arguments: args } }); if (!result) return null; - // Le tool retourne content[0] (summary) et content[1] (JSON) if (result.content) { - const jsonContent = result.content.find(c => { try { JSON.parse(c.text); return true; } catch { return false; } }); - if (jsonContent) { try { return JSON.parse(jsonContent.text); } catch {} } + for (const c of result.content) { try { return JSON.parse(c.text); } catch {} } return result.content[0]?.text; } return result; } -async function shape(type, args) { - const r = await callTool(`create_${type}`, args); - const id = typeof r === 'object' ? r?.id : null; - console.log(` ✓ ${type} "${args.name || ''}" ${id ? '→ ' + id.slice(0,8) : JSON.stringify(r).slice(0,60)}`); +async function rect(args) { + const r = await callTool('create_rectangle', args); + console.log(` ▪ rect "${args.name}" ${r?.id?.slice(0,8) || JSON.stringify(r).slice(0,40)}`); + return r; +} + +async function txt(args) { + const r = await callTool('create_text', args); + console.log(` ▪ text "${args.name}" ${r?.id?.slice(0,8) || JSON.stringify(r).slice(0,40)}`); return r; } // ─── MAIN ──────────────────────────────────────────────────────────────────── -console.log('🚀 Démarrage création affiche SOLDES...'); +console.log('🚀 Création affiche SOLDES (version corrigée)...'); await restartMCPContainer(); await sleep(10000); await initMCP(); -// Profil +// Profil + IDs const profile = await callTool('get_profile', {}); const projectId = profile?.defaultProjectId; console.log('ProjectId:', projectId); -// Créer le fichier (camelCase!) -console.log('\n📄 Création du fichier...'); -const file = await callTool('create_file', { projectId, name: 'Affiche SOLDES -50%' }); -console.log('File:', JSON.stringify(file).slice(0, 150)); +// Supprimer l'ancien fichier si présent +const existingFiles = await callTool('list_files', { projectId }); +if (Array.isArray(existingFiles)) { + for (const f of existingFiles.filter(f => f.name?.includes('SOLDES'))) { + await callTool('delete_file', { fileId: f.id }); + console.log('🗑 Supprimé ancien fichier:', f.name); + } +} +// Créer le fichier +const file = await callTool('create_file', { projectId, name: 'Affiche SOLDES -50%' }); const fileId = file?.id; -if (!fileId) { console.error('❌ Pas de fileId'); process.exit(1); } console.log('FileId:', fileId); -// Lister les pages (camelCase) -const pagesResult = await callTool('list_pages', { fileId }); -console.log('Pages raw:', JSON.stringify(pagesResult).slice(0, 200)); -let pageId = null; -if (Array.isArray(pagesResult)) pageId = pagesResult[0]?.id; -else if (pagesResult?.pages) pageId = pagesResult.pages[0]?.id; -else { - // Extraire UUID depuis texte - const uuids = JSON.stringify(pagesResult).match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g); - pageId = uuids?.find(u => u !== fileId); -} +const pages = await callTool('list_pages', { fileId }); +const pageId = Array.isArray(pages) ? pages[0]?.id : null; console.log('PageId:', pageId); if (!pageId) { console.error('❌ Pas de pageId'); process.exit(1); } -// ─── DESIGN (600×900) ──────────────────────────────────────────────────────── -console.log('\n🎨 Construction du design...'); +// ─── DESIGN 600×900 — frame à (0,0) ───────────────────────────────────────── +console.log('\n🎨 Construction...'); -// Frame principale - fond rouge -const mainFrame = await shape('frame', { +// Frame principale = fond rouge (à 0,0 → coords absolues = relatives) +const frame = await callTool('create_frame', { fileId, pageId, - name: 'Affiche SOLDES', - x: 100, y: 100, - width: 600, height: 900, + name: 'Affiche SOLDES', x: 0, y: 0, width: 600, height: 900, fillColor: '#C0392B', }); -const frameId = mainFrame?.id; +const frameId = frame?.id; -// Bandes jaunes déco haut/bas -await shape('rectangle', { fileId, pageId, parentId: frameId, name: 'Bande haut', x: 0, y: 0, width: 600, height: 14, fillColor: '#F1C40F' }); -await shape('rectangle', { fileId, pageId, parentId: frameId, name: 'Bande bas', x: 0, y: 886, width: 600, height: 14, fillColor: '#F1C40F' }); +// ── Bandes jaunes haut / bas (parentId OK pour rect) +await rect({ fileId, pageId, parentId: frameId, name: 'Bande haut', x: 0, y: 0, width: 600, height: 14, fillColor: '#F1C40F' }); +await rect({ fileId, pageId, parentId: frameId, name: 'Bande bas', x: 0, y: 886, width: 600, height: 14, fillColor: '#F1C40F' }); -// Header sombre -await shape('rectangle', { fileId, pageId, parentId: frameId, name: 'Header bg', x: 0, y: 14, width: 600, height: 88, fillColor: '#922B21' }); +// ── Header sombre +await rect({ fileId, pageId, parentId: frameId, name: 'Header bg', x: 0, y: 14, width: 600, height: 88, fillColor: '#7B241C' }); -// MEGA SOLDES -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'MEGA SOLDES', content: 'MEGA SOLDES', - x: 0, y: 28, width: 600, height: 65, - fontSize: 36, fontWeight: '800', fillColor: '#F1C40F', align: 'center', -}); +// ── Séparateur milieu +await rect({ fileId, pageId, parentId: frameId, name: 'Sep1', x: 80, y: 430, width: 440, height: 3, fillColor: '#F1C40F' }); + +// ── Badge OFFRE LIMITÉE +await rect({ fileId, pageId, parentId: frameId, name: 'Badge bg', x: 115, y: 450, width: 370, height: 68, fillColor: '#F1C40F', r1: 34, r2: 34, r3: 34, r4: 34 }); + +// ── Séparateur bas +await rect({ fileId, pageId, parentId: frameId, name: 'Sep2', x: 80, y: 760, width: 440, height: 3, fillColor: '#F8C471' }); + +// ── TEXTES (coords absolues, pas de parentId) + +// Collection header (petit texte dans le header sombre) +await txt({ fileId, pageId, name: 'Collection', text: '✦ Collection Printemps 2026 ✦', x: 0, y: 32, fontSize: 14, fontWeight: '700', fillColor: '#F1C40F', textAlign: 'center', letterSpacing: 4 }); + +// SOLDES (grand titre) +await txt({ fileId, pageId, name: 'Soldes', text: 'SOLDES', x: 0, y: 115, fontSize: 90, fontWeight: '900', fillColor: '#F1C40F', textAlign: 'center', letterSpacing: 10 }); // -50% -await shape('text', { - fileId, pageId, parentId: frameId, - name: '-50%', content: '-50%', - x: 0, y: 110, width: 600, height: 250, - fontSize: 180, fontWeight: '900', fillColor: '#FFFFFF', align: 'center', -}); +await txt({ fileId, pageId, name: 'Pct', text: '-50%', x: 0, y: 205, fontSize: 190, fontWeight: '900', fillColor: '#FFFFFF', textAlign: 'center' }); -// Sous-titre chaussures -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Sous-titre', content: 'SUR TOUTES NOS CHAUSSURES', - x: 0, y: 368, width: 600, height: 55, - fontSize: 24, fontWeight: '700', fillColor: '#F1C40F', align: 'center', -}); +// Sous-titre +await txt({ fileId, pageId, name: 'Sous-titre', text: 'SUR TOUTES NOS CHAUSSURES', x: 0, y: 380, fontSize: 22, fontWeight: '700', fillColor: '#F1C40F', textAlign: 'center', letterSpacing: 3 }); -// Séparateur -await shape('rectangle', { fileId, pageId, parentId: frameId, name: 'Sep', x: 80, y: 432, width: 440, height: 3, fillColor: '#F1C40F' }); - -// Badge offre -await shape('rectangle', { - fileId, pageId, parentId: frameId, - name: 'Badge bg', x: 125, y: 450, width: 350, height: 70, - fillColor: '#F1C40F', r1: 12, r2: 12, r3: 12, r4: 12, -}); -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Badge text', content: '⚡ OFFRE LIMITÉE ⚡', - x: 125, y: 465, width: 350, height: 42, - fontSize: 22, fontWeight: '800', fillColor: '#922B21', align: 'center', -}); +// Offre limitée (sur le badge jaune) +await txt({ fileId, pageId, name: 'Offre', text: '⚡ OFFRE LIMITÉE ⚡', x: 115, y: 468, fontSize: 22, fontWeight: '900', fillColor: '#7B241C', textAlign: 'center' }); // Dates -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Dates', content: 'du 1er au 31 mars 2026', - x: 0, y: 540, width: 600, height: 40, - fontSize: 20, fontWeight: '400', fillColor: '#FADBD8', align: 'center', -}); +await txt({ fileId, pageId, name: 'Dates', text: 'du 1er au 31 mars 2026', x: 0, y: 542, fontSize: 16, fontWeight: '400', fillColor: '#FADBD8', textAlign: 'center', letterSpacing: 2 }); -// Emojis chaussures -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Chaussures', content: '👟 👠 👞', - x: 0, y: 590, width: 600, height: 160, - fontSize: 90, align: 'center', -}); - -// Ligne déco -await shape('rectangle', { fileId, pageId, parentId: frameId, name: 'Sep2', x: 80, y: 760, width: 440, height: 3, fillColor: '#F8C471' }); +// Emojis +await txt({ fileId, pageId, name: 'Chaussures', text: '👟 👠 👞', x: 0, y: 580, fontSize: 80, textAlign: 'center' }); // Site web -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Site web', content: 'www.votre-boutique.fr', - x: 0, y: 775, width: 600, height: 35, - fontSize: 16, fontWeight: '400', fillColor: '#F8C471', align: 'center', -}); +await txt({ fileId, pageId, name: 'Site web', text: 'www.votre-boutique.fr', x: 0, y: 780, fontSize: 14, fontWeight: '400', fillColor: '#F8C471', textAlign: 'center', letterSpacing: 3 }); -// Étoiles déco bas -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Etoiles', content: '★ ★ ★ ★ ★', - x: 0, y: 830, width: 600, height: 40, - fontSize: 22, fillColor: '#F1C40F', align: 'center', -}); +// Étoiles +await txt({ fileId, pageId, name: 'Etoiles', text: '★ ★ ★ ★ ★', x: 0, y: 836, fontSize: 20, fillColor: '#F1C40F', textAlign: 'center' }); -console.log('\n✅ Affiche SOLDES créée dans Penpot !'); -console.log('🔗 http://192.168.1.150:9001 → dossier "Drafts" → "Affiche SOLDES -50%"'); +console.log('\n✅ Affiche recréée dans Penpot !'); +console.log('🔗 http://192.168.1.150:9001 → Drafts → "Affiche SOLDES -50%"'); diff --git a/penpot-template.mjs b/penpot-template.mjs index b2bcc3d..a49c6e9 100644 --- a/penpot-template.mjs +++ b/penpot-template.mjs @@ -1,6 +1,10 @@ /** - * Script tout-en-un : redémarre le MCP + crée l'affiche SOLDES - * Paramètres MCP = camelCase ! + * Affiche SOLDES -50% — version corrigée + * Leçons apprises : + * - create_text : paramètre `text` (pas `content`), `textAlign` (pas `align`) + * - create_text : parentId NON supporté → textes à coords absolues + * - create_rectangle : parentId OK + * - Frame à (0,0) → coordonnées absolues = relatives au frame */ import https from 'https'; import http from 'http'; @@ -17,8 +21,7 @@ function portainerReq(path, method = 'GET') { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve(res.statusCode); } }); }); - req.on('error', reject); - req.end(); + req.on('error', reject); req.end(); }); } @@ -56,21 +59,14 @@ function postMCP(body) { if (line.startsWith('data:')) { const raw = line.slice(5).trim(); if (!raw || raw === '[DONE]') continue; - try { - const j = JSON.parse(raw); - if (j.result !== undefined) result = j.result; - if (j.error) error = j.error; - } catch {} + try { const j = JSON.parse(raw); if (j.result !== undefined) result = j.result; if (j.error) error = j.error; } catch {} } } if (error) reject(new Error(JSON.stringify(error))); else resolve(result); } else { - try { - const j = JSON.parse(data); - if (j.error) reject(new Error(JSON.stringify(j.error))); - else resolve(j.result ?? j); - } catch { resolve(data || null); } + try { const j = JSON.parse(data); if (j.error) reject(new Error(JSON.stringify(j.error))); else resolve(j.result ?? j); } + catch { resolve(data || null); } } }); }); @@ -81,156 +77,116 @@ function postMCP(body) { async function initMCP() { await postMCP({ jsonrpc: '2.0', id: msgId++, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'nox', version: '1.0' } } }); await postMCP({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }).catch(() => {}); - console.log('✅ MCP Session:', sessionId); + console.log('✅ Session:', sessionId); } async function callTool(name, args = {}) { const result = await postMCP({ jsonrpc: '2.0', id: msgId++, method: 'tools/call', params: { name, arguments: args } }); if (!result) return null; - // Le tool retourne content[0] (summary) et content[1] (JSON) if (result.content) { - const jsonContent = result.content.find(c => { try { JSON.parse(c.text); return true; } catch { return false; } }); - if (jsonContent) { try { return JSON.parse(jsonContent.text); } catch {} } + for (const c of result.content) { try { return JSON.parse(c.text); } catch {} } return result.content[0]?.text; } return result; } -async function shape(type, args) { - const r = await callTool(`create_${type}`, args); - const id = typeof r === 'object' ? r?.id : null; - console.log(` ✓ ${type} "${args.name || ''}" ${id ? '→ ' + id.slice(0,8) : JSON.stringify(r).slice(0,60)}`); +async function rect(args) { + const r = await callTool('create_rectangle', args); + console.log(` ▪ rect "${args.name}" ${r?.id?.slice(0,8) || JSON.stringify(r).slice(0,40)}`); + return r; +} + +async function txt(args) { + const r = await callTool('create_text', args); + console.log(` ▪ text "${args.name}" ${r?.id?.slice(0,8) || JSON.stringify(r).slice(0,40)}`); return r; } // ─── MAIN ──────────────────────────────────────────────────────────────────── -console.log('🚀 Démarrage création affiche SOLDES...'); +console.log('🚀 Création affiche SOLDES (version corrigée)...'); await restartMCPContainer(); await sleep(10000); await initMCP(); -// Profil +// Profil + IDs const profile = await callTool('get_profile', {}); const projectId = profile?.defaultProjectId; console.log('ProjectId:', projectId); -// Créer le fichier (camelCase!) -console.log('\n📄 Création du fichier...'); -const file = await callTool('create_file', { projectId, name: 'Affiche SOLDES -50%' }); -console.log('File:', JSON.stringify(file).slice(0, 150)); +// Supprimer l'ancien fichier si présent +const existingFiles = await callTool('list_files', { projectId }); +if (Array.isArray(existingFiles)) { + for (const f of existingFiles.filter(f => f.name?.includes('SOLDES'))) { + await callTool('delete_file', { fileId: f.id }); + console.log('🗑 Supprimé ancien fichier:', f.name); + } +} +// Créer le fichier +const file = await callTool('create_file', { projectId, name: 'Affiche SOLDES -50%' }); const fileId = file?.id; -if (!fileId) { console.error('❌ Pas de fileId'); process.exit(1); } console.log('FileId:', fileId); -// Lister les pages (camelCase) -const pagesResult = await callTool('list_pages', { fileId }); -console.log('Pages raw:', JSON.stringify(pagesResult).slice(0, 200)); -let pageId = null; -if (Array.isArray(pagesResult)) pageId = pagesResult[0]?.id; -else if (pagesResult?.pages) pageId = pagesResult.pages[0]?.id; -else { - // Extraire UUID depuis texte - const uuids = JSON.stringify(pagesResult).match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g); - pageId = uuids?.find(u => u !== fileId); -} +const pages = await callTool('list_pages', { fileId }); +const pageId = Array.isArray(pages) ? pages[0]?.id : null; console.log('PageId:', pageId); if (!pageId) { console.error('❌ Pas de pageId'); process.exit(1); } -// ─── DESIGN (600×900) ──────────────────────────────────────────────────────── -console.log('\n🎨 Construction du design...'); +// ─── DESIGN 600×900 — frame à (0,0) ───────────────────────────────────────── +console.log('\n🎨 Construction...'); -// Frame principale - fond rouge -const mainFrame = await shape('frame', { +// Frame principale = fond rouge (à 0,0 → coords absolues = relatives) +const frame = await callTool('create_frame', { fileId, pageId, - name: 'Affiche SOLDES', - x: 100, y: 100, - width: 600, height: 900, + name: 'Affiche SOLDES', x: 0, y: 0, width: 600, height: 900, fillColor: '#C0392B', }); -const frameId = mainFrame?.id; +const frameId = frame?.id; -// Bandes jaunes déco haut/bas -await shape('rectangle', { fileId, pageId, parentId: frameId, name: 'Bande haut', x: 0, y: 0, width: 600, height: 14, fillColor: '#F1C40F' }); -await shape('rectangle', { fileId, pageId, parentId: frameId, name: 'Bande bas', x: 0, y: 886, width: 600, height: 14, fillColor: '#F1C40F' }); +// ── Bandes jaunes haut / bas (parentId OK pour rect) +await rect({ fileId, pageId, parentId: frameId, name: 'Bande haut', x: 0, y: 0, width: 600, height: 14, fillColor: '#F1C40F' }); +await rect({ fileId, pageId, parentId: frameId, name: 'Bande bas', x: 0, y: 886, width: 600, height: 14, fillColor: '#F1C40F' }); -// Header sombre -await shape('rectangle', { fileId, pageId, parentId: frameId, name: 'Header bg', x: 0, y: 14, width: 600, height: 88, fillColor: '#922B21' }); +// ── Header sombre +await rect({ fileId, pageId, parentId: frameId, name: 'Header bg', x: 0, y: 14, width: 600, height: 88, fillColor: '#7B241C' }); -// MEGA SOLDES -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'MEGA SOLDES', content: 'MEGA SOLDES', - x: 0, y: 28, width: 600, height: 65, - fontSize: 36, fontWeight: '800', fillColor: '#F1C40F', align: 'center', -}); +// ── Séparateur milieu +await rect({ fileId, pageId, parentId: frameId, name: 'Sep1', x: 80, y: 430, width: 440, height: 3, fillColor: '#F1C40F' }); + +// ── Badge OFFRE LIMITÉE +await rect({ fileId, pageId, parentId: frameId, name: 'Badge bg', x: 115, y: 450, width: 370, height: 68, fillColor: '#F1C40F', r1: 34, r2: 34, r3: 34, r4: 34 }); + +// ── Séparateur bas +await rect({ fileId, pageId, parentId: frameId, name: 'Sep2', x: 80, y: 760, width: 440, height: 3, fillColor: '#F8C471' }); + +// ── TEXTES (coords absolues, pas de parentId) + +// Collection header (petit texte dans le header sombre) +await txt({ fileId, pageId, name: 'Collection', text: '✦ Collection Printemps 2026 ✦', x: 0, y: 32, fontSize: 14, fontWeight: '700', fillColor: '#F1C40F', textAlign: 'center', letterSpacing: 4 }); + +// SOLDES (grand titre) +await txt({ fileId, pageId, name: 'Soldes', text: 'SOLDES', x: 0, y: 115, fontSize: 90, fontWeight: '900', fillColor: '#F1C40F', textAlign: 'center', letterSpacing: 10 }); // -50% -await shape('text', { - fileId, pageId, parentId: frameId, - name: '-50%', content: '-50%', - x: 0, y: 110, width: 600, height: 250, - fontSize: 180, fontWeight: '900', fillColor: '#FFFFFF', align: 'center', -}); +await txt({ fileId, pageId, name: 'Pct', text: '-50%', x: 0, y: 205, fontSize: 190, fontWeight: '900', fillColor: '#FFFFFF', textAlign: 'center' }); -// Sous-titre chaussures -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Sous-titre', content: 'SUR TOUTES NOS CHAUSSURES', - x: 0, y: 368, width: 600, height: 55, - fontSize: 24, fontWeight: '700', fillColor: '#F1C40F', align: 'center', -}); +// Sous-titre +await txt({ fileId, pageId, name: 'Sous-titre', text: 'SUR TOUTES NOS CHAUSSURES', x: 0, y: 380, fontSize: 22, fontWeight: '700', fillColor: '#F1C40F', textAlign: 'center', letterSpacing: 3 }); -// Séparateur -await shape('rectangle', { fileId, pageId, parentId: frameId, name: 'Sep', x: 80, y: 432, width: 440, height: 3, fillColor: '#F1C40F' }); - -// Badge offre -await shape('rectangle', { - fileId, pageId, parentId: frameId, - name: 'Badge bg', x: 125, y: 450, width: 350, height: 70, - fillColor: '#F1C40F', r1: 12, r2: 12, r3: 12, r4: 12, -}); -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Badge text', content: '⚡ OFFRE LIMITÉE ⚡', - x: 125, y: 465, width: 350, height: 42, - fontSize: 22, fontWeight: '800', fillColor: '#922B21', align: 'center', -}); +// Offre limitée (sur le badge jaune) +await txt({ fileId, pageId, name: 'Offre', text: '⚡ OFFRE LIMITÉE ⚡', x: 115, y: 468, fontSize: 22, fontWeight: '900', fillColor: '#7B241C', textAlign: 'center' }); // Dates -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Dates', content: 'du 1er au 31 mars 2026', - x: 0, y: 540, width: 600, height: 40, - fontSize: 20, fontWeight: '400', fillColor: '#FADBD8', align: 'center', -}); +await txt({ fileId, pageId, name: 'Dates', text: 'du 1er au 31 mars 2026', x: 0, y: 542, fontSize: 16, fontWeight: '400', fillColor: '#FADBD8', textAlign: 'center', letterSpacing: 2 }); -// Emojis chaussures -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Chaussures', content: '👟 👠 👞', - x: 0, y: 590, width: 600, height: 160, - fontSize: 90, align: 'center', -}); - -// Ligne déco -await shape('rectangle', { fileId, pageId, parentId: frameId, name: 'Sep2', x: 80, y: 760, width: 440, height: 3, fillColor: '#F8C471' }); +// Emojis +await txt({ fileId, pageId, name: 'Chaussures', text: '👟 👠 👞', x: 0, y: 580, fontSize: 80, textAlign: 'center' }); // Site web -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Site web', content: 'www.votre-boutique.fr', - x: 0, y: 775, width: 600, height: 35, - fontSize: 16, fontWeight: '400', fillColor: '#F8C471', align: 'center', -}); +await txt({ fileId, pageId, name: 'Site web', text: 'www.votre-boutique.fr', x: 0, y: 780, fontSize: 14, fontWeight: '400', fillColor: '#F8C471', textAlign: 'center', letterSpacing: 3 }); -// Étoiles déco bas -await shape('text', { - fileId, pageId, parentId: frameId, - name: 'Etoiles', content: '★ ★ ★ ★ ★', - x: 0, y: 830, width: 600, height: 40, - fontSize: 22, fillColor: '#F1C40F', align: 'center', -}); +// Étoiles +await txt({ fileId, pageId, name: 'Etoiles', text: '★ ★ ★ ★ ★', x: 0, y: 836, fontSize: 20, fillColor: '#F1C40F', textAlign: 'center' }); -console.log('\n✅ Affiche SOLDES créée dans Penpot !'); -console.log('🔗 http://192.168.1.150:9001 → dossier "Drafts" → "Affiche SOLDES -50%"'); +console.log('\n✅ Affiche recréée dans Penpot !'); +console.log('🔗 http://192.168.1.150:9001 → Drafts → "Affiche SOLDES -50%"');