Compare commits

..

5 Commits

17 changed files with 1040 additions and 59 deletions
+6
View File
@@ -6,6 +6,7 @@ depuis layouts_v2.yaml. Relancer après tout ajout de layout.
Usage : python3 build_gallery.py → produit index.html
"""
import yaml
import wireframes
from pathlib import Path
from datetime import datetime
@@ -35,6 +36,7 @@ def card(name, cfg):
hint = " ".join(cfg.get("agent_hint", "").split())
champs = ", ".join(cfg.get("champs", []))
requis = ", ".join(cfg.get("champs_requis", []))
thumb = wireframes.svg_for(name, mode)
mode_badge = {
"light": ("Clair", "#f4f1ec", NAVY),
"dark": ("Sombre", NAVY, "#ffffff"),
@@ -46,6 +48,7 @@ def card(name, cfg):
<span class="lcode">{lid}</span>
<span class="lmode" style="background:{mode_badge[1]};color:{mode_badge[2]}">{mode_badge[0]}</span>
</div>
<div class="wf-thumb">{thumb}</div>
<h3>{name}</h3>
<p class="hint">{hint}</p>
<div class="fields">
@@ -82,6 +85,7 @@ html = f"""<!DOCTYPE html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Sliding Design System v2 — Pernod Ricard</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cambria&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
@@ -113,6 +117,8 @@ h1{{font-family:Cambria,serif;font-size:60px;font-weight:700;line-height:1.05;le
.lg{{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:24px}}
.lc{{background:#fff;border:1px solid var(--bo);border-radius:10px;padding:24px;transition:all .25s;display:flex;flex-direction:column;box-shadow:0 1px 3px rgba(6,16,51,.04)}}
.lc:hover{{transform:translateY(-3px);box-shadow:0 12px 36px rgba(6,16,51,.10);border-color:var(--coral)}}
.wf-thumb{{margin:0 0 14px;border:1px solid #ECE9E2;border-radius:8px;overflow:hidden;background:#fff}}
.wf-thumb svg{{display:block;width:100%;height:auto}}
.lc-head{{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}}
.lcode{{background:var(--navy);color:#fff;font-size:10px;font-weight:600;padding:3px 9px;border-radius:4px;letter-spacing:.1em;font-family:monospace}}
.lmode{{font-size:10px;font-weight:600;padding:3px 10px;border-radius:20px;letter-spacing:.05em}}
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
check_goldens.py — C0 · goldens de non-régression du moteur
===========================================================
Rend chaque golden_*.yaml présent et compare sa STRUCTURE (nb de
slides, nb de shapes par slide) au manifeste goldens_manifest.json.
Le PPTX contient des timestamps : un diff binaire est impossible — la
structure, elle, est déterministe et attrape les régressions de rendu
(shape manquante, layout cassé, exception).
python3 check_goldens.py → vérifie (code retour 1 si écart)
python3 check_goldens.py --update → (re)génère le manifeste
À lancer avant tout push moteur (ou en tâche DSM hebdo).
"""
import glob
import json
import subprocess
import sys
from pathlib import Path
MANIFEST = Path("goldens_manifest.json")
def structure(pptx_path):
from pptx import Presentation
prs = Presentation(pptx_path)
return [len(list(s.shapes)) for s in prs.slides]
def render_all():
out = {}
for y in sorted(glob.glob("golden_*.yaml")):
pptx = "_check_%s.pptx" % Path(y).stem
r = subprocess.run(
["python3", "render_engine_v2.py", y, pptx,
"--assets", "assets"],
capture_output=True, text=True)
if r.returncode != 0:
print(" ! %s : le rendu ÉCHOUE\n%s" % (y, r.stdout[-400:]))
sys.exit(1)
out[y] = structure(pptx)
Path(pptx).unlink(missing_ok=True)
return out
def main():
got = render_all()
if "--update" in sys.argv:
MANIFEST.write_text(json.dumps(got, indent=2))
print(" + Manifeste écrit : %d goldens, %d slides au total."
% (len(got), sum(len(v) for v in got.values())))
return
if not MANIFEST.exists():
print(" ! Pas de manifeste — lancer d'abord : "
"python3 check_goldens.py --update")
sys.exit(1)
want = json.loads(MANIFEST.read_text())
ok = True
for y, shapes in got.items():
ref = want.get(y)
if ref is None:
print(" ~ %s : nouveau golden (absent du manifeste)" % y)
continue
if shapes != ref:
ok = False
print(" ! %s : ÉCART — slides/shapes %s ≠ attendu %s"
% (y, shapes, ref))
for y in want:
if y not in got:
ok = False
print(" ! %s : golden du manifeste INTROUVABLE" % y)
print(" %s" % ("✓ goldens conformes (%d fichiers)" % len(got)
if ok else "✗ régression détectée"))
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()
+115 -15
View File
@@ -47,7 +47,11 @@ SYSTEM_PROMPT = (
"d'important ; condenser légèrement si nécessaire.\n"
"- Texte brut uniquement : aucun balisage Markdown (pas de **, __, #, `, "
"ni tirets de liste dans les valeurs).\n"
"- Français, ton affirmatif, guillemets typographiques évités.\n"
"- LANGUE : conserve strictement la langue du contenu fourni (un plan "
"anglais donne des champs 100 % anglais). Ces instructions sont en "
"français mais ce n'est JAMAIS la langue du livrable. Ne traduis "
"rien.\n"
"- Ton affirmatif, guillemets typographiques évités.\n"
"- Champs optionnels sans contenu correspondant : null.\n"
"- Les nombres des champs numériques (start, end, x, y, numero) sont des "
"entiers, pas des chaînes.\n"
@@ -58,7 +62,22 @@ SYSTEM_PROMPT = (
"ignore-les."
)
# "SLIDE 12 — layout_name" (tiret cadratin, demi-cadratin ou simple)
# FORMAT RÉEL du Designer (prompt_the_designer_v3) : le Markdown du
# Narrator repris intégralement, avec une ligne "@layout: nom" ajoutée
# sous chaque titre de slide, et des "@note:" facultatives.
# ### 3. The CEO's 3 priorities
# @layout: two_cols_text
# @note: ...
# <contenu>
# On découpe donc sur les lignes @layout, et on déduit la position du
# titre qui précède (### 3. …) ou, à défaut, de l'ordre d'apparition.
LAYOUT_RE = re.compile(r"^[ \t]*@layout[ \t]*:[ \t]*([a-z][a-z0-9_]*)[ \t]*$",
re.MULTILINE)
# Titre de slide : "### 12. Titre" ou "### [OPT-EXEC] 5. Titre"
TITLE_NUM_RE = re.compile(
r"^[ \t]*#{1,6}[ \t]*(?:\[[^\]]+\][ \t]*)?(\d+)[.)][ \t]*",
re.MULTILINE)
# Compat ascendante : ancien format "SLIDE 12 — layout_name"
SLIDE_RE = re.compile(
r"^\s*SLIDE\s+(\d+)\s*[—–\-]+\s*([a-z][a-z0-9_]*)\s*$",
re.MULTILINE)
@@ -72,22 +91,60 @@ MD_PATTERNS = [
# ── Découpage du plan ────────────────────────────────────────────────────────
def _position_before(plan_md, idx, fallback):
"""Numéro de la dernière ligne de titre '### N.' avant idx."""
last = None
for m in TITLE_NUM_RE.finditer(plan_md, 0, idx):
last = m
if last:
try:
return int(last.group(1))
except (TypeError, ValueError):
pass
return fallback
def split_plan(plan_md):
"""Découpe le plan Designer en slides.
"""Découpe le plan annoté par le Designer en slides.
Format principal : lignes '@layout: nom'. Compat : 'SLIDE N — nom'.
Retourne (segments, errors) ; segment = {position, layout, content}."""
matches = list(SLIDE_RE.finditer(plan_md))
segments, errors = [], []
if not matches:
return [], ["Aucune ligne 'SLIDE N — layout' détectée dans le plan."]
valid = set(known_layouts())
matches = list(LAYOUT_RE.finditer(plan_md))
if matches:
for i, m in enumerate(matches):
start = m.start()
end = (matches[i + 1].start() if i + 1 < len(matches)
else len(plan_md))
layout = m.group(1)
pos = _position_before(plan_md, start, i + 1)
seg = {"position": pos, "layout": layout,
"content": plan_md[start:end].strip()}
if layout not in valid:
errors.append("Slide %d : layout '%s' sans schéma (connus"
" : %s)" % (pos, layout,
", ".join(sorted(valid))))
continue
segments.append(seg)
# Positions dupliquées (titre absent) → renumérotation d'ordre
seen = [s["position"] for s in segments]
if len(set(seen)) != len(seen):
for i, s in enumerate(segments, start=1):
s["position"] = i
return segments, errors
# Fallback : ancien format explicite
matches = list(SLIDE_RE.finditer(plan_md))
if not matches:
return [], ["Aucune annotation '@layout: nom' (ni 'SLIDE N — "
"layout') détectée dans le plan du Designer."]
for i, m in enumerate(matches):
end = matches[i + 1].start() if i + 1 < len(matches) else len(plan_md)
end = (matches[i + 1].start() if i + 1 < len(matches)
else len(plan_md))
layout = m.group(2)
seg = {
"position": int(m.group(1)),
"layout": layout,
"content": plan_md[m.start():end].strip(),
}
seg = {"position": int(m.group(1)), "layout": layout,
"content": plan_md[m.start():end].strip()}
if layout not in valid:
errors.append("Slide %d : layout '%s' sans schéma (connus : %s)"
% (seg["position"], layout,
@@ -121,7 +178,7 @@ def _post_with_retry(payload, api_key):
% (MAX_RETRY, last_err))
def encode_slide(content, layout, api_key, model=None):
def encode_slide(content, layout, api_key, model=None, lang=None):
"""Encode UNE slide. Retourne (dict_contenu, usage_dict).
Lève RuntimeError/ValueError en cas d'échec (isolé par l'appelant)."""
schema = get_schema(layout)
@@ -131,7 +188,9 @@ def encode_slide(content, layout, api_key, model=None):
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content":
"Layout : %s\n\nContenu de la slide :\n%s"
("LANGUE DU DECK : %s — tous les champs texte restent "
"dans cette langue.\n" % lang if lang else "")
+ "Layout : %s\n\nContenu de la slide :\n%s"
% (layout, content)},
],
"response_format": {
@@ -181,6 +240,41 @@ def _accumulate(total, usage):
return total
def detect_lang(text):
"""Heuristique stopwords : 'anglais' / 'français' / None (mixte)."""
t = " %s " % re.sub(r"\s+", " ", text.lower())
en = sum(t.count(w) for w in (" the ", " and ", " of ", " to ",
" is ", " for ", " with "))
fr = sum(t.count(w) for w in (" le ", " la ", " les ", " des ",
" est ", " pour ", " avec ", " une "))
if en > fr * 1.5:
return "anglais"
if fr > en * 1.5:
return "français"
return None
TITLE_PREFIX_RE = re.compile(r"^\s*(?:\[[^\]]+\]\s*)?\d+[.)]\s*")
def strip_title_number(content):
"""Retire la numérotation Markdown résiduelle du titre ('13. X''X')."""
t = content.get("titre")
if isinstance(t, str):
content["titre"] = TITLE_PREFIX_RE.sub("", t)
return content
def trim_trailing_meta(segments):
"""Le DERNIER segment court jusqu'à la fin du texte : il peut embarquer
les commentaires de conclusion du Designer (après un '---'). On coupe."""
if segments:
parts = segments[-1]["content"].split("\n---")
if parts[0].count("\n") >= 1:
segments[-1]["content"] = parts[0].rstrip()
return segments
# ── Orchestration ────────────────────────────────────────────────────────────
def encode_plan(plan_md, api_key, model=None, only_positions=None,
@@ -190,6 +284,7 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
progress : callable(str) pour l'affichage (info du facilitator).
Retourne (data, usage, errors) :
data = {"titre_presentation": str|None, "slides": [...]}
La langue du plan est détectée et imposée à chaque slide (anti-dérive).
usage = tokens cumulés {"prompt_tokens","completion_tokens","total_tokens"}
errors = liste de messages (slides en échec — absentes de data).
"""
@@ -198,6 +293,10 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
progress(msg)
segments, errors = split_plan(plan_md)
segments = trim_trailing_meta(segments)
lang = detect_lang(plan_md)
if lang:
say("Langue du deck : %s (imposée à chaque slide)" % lang)
if only_positions is not None:
wanted = set(int(p) for p in only_positions)
segments = [s for s in segments if s["position"] in wanted]
@@ -207,7 +306,7 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
say("Slide %d (%s)..." % (seg["position"], seg["layout"]))
try:
content, u = encode_slide(seg["content"], seg["layout"],
api_key, model)
api_key, model, lang=lang)
except (RuntimeError, ValueError, KeyError,
json.JSONDecodeError) as e:
errors.append("Slide %d (%s) : %s"
@@ -215,6 +314,7 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
continue
_accumulate(usage, u)
content = strip_md(drop_nulls(content))
content = strip_title_number(content)
if seg["layout"] == "cover_split" and titre_presentation is None:
titre_presentation = content.get("titre")
slide = {"position": seg["position"], "layout": seg["layout"]}
+1
View File
@@ -0,0 +1 @@
{}
+35 -1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Sliding Design System v2 — Pernod Ricard</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cambria&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
@@ -35,6 +36,8 @@ h1{font-family:Cambria,serif;font-size:60px;font-weight:700;line-height:1.05;let
.lg{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:24px}
.lc{background:#fff;border:1px solid var(--bo);border-radius:10px;padding:24px;transition:all .25s;display:flex;flex-direction:column;box-shadow:0 1px 3px rgba(6,16,51,.04)}
.lc:hover{transform:translateY(-3px);box-shadow:0 12px 36px rgba(6,16,51,.10);border-color:var(--coral)}
.wf-thumb{margin:0 0 14px;border:1px solid #ECE9E2;border-radius:8px;overflow:hidden;background:#fff}
.wf-thumb svg{display:block;width:100%;height:auto}
.lc-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
.lcode{background:var(--navy);color:#fff;font-size:10px;font-weight:600;padding:3px 9px;border-radius:4px;letter-spacing:.1em;font-family:monospace}
.lmode{font-size:10px;font-weight:600;padding:3px 10px;border-radius:20px;letter-spacing:.05em}
@@ -79,6 +82,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L01</span>
<span class="lmode" style="background:#061033;color:#ffffff">Sombre</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#061033" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><circle cx="300" cy="20" r="46" fill="#10204D" opacity="1.0"/><circle cx="288" cy="150" r="30" fill="#F4795B" opacity="0.9"/><circle cx="250" cy="150" r="14" fill="#8FA9D0" opacity="1.0"/><rect x="20" y="74" width="40" height="4" rx="1" fill="#F4795B" opacity="1.0"/><rect x="20" y="88" width="150" height="16" rx="2" fill="#FFFFFF" opacity="1.0"/><rect x="20" y="120" width="96" height="8" rx="2" fill="#8FA9D0" opacity="1.0"/></svg></div>
<h3>cover_split</h3>
<p class="hint">Slide de couverture. titre = titre de la présentation, sous_titre = tagline en une phrase.</p>
<div class="fields">
@@ -100,6 +104,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L05</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="160" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="54" width="96" height="108" rx="6" fill="#F4F1EC" opacity="1.0"/><circle cx="36" cy="74" r="9" fill="#061033" opacity="1.0"/><rect x="32" y="96" width="72" height="7" rx="3" fill="#46505A" opacity="1.0"/><rect x="32" y="112" width="72" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="32" y="124" width="72" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="32" y="136" width="72" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="112" y="54" width="96" height="108" rx="6" fill="#F4F1EC" opacity="1.0"/><circle cx="128" cy="74" r="9" fill="#061033" opacity="1.0"/><rect x="124" y="96" width="72" height="7" rx="3" fill="#46505A" opacity="1.0"/><rect x="124" y="112" width="72" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="124" y="124" width="72" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="124" y="136" width="72" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="204" y="54" width="96" height="108" rx="6" fill="#F4F1EC" opacity="1.0"/><circle cx="220" cy="74" r="9" fill="#061033" opacity="1.0"/><rect x="216" y="96" width="72" height="7" rx="3" fill="#46505A" opacity="1.0"/><rect x="216" y="112" width="72" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="216" y="124" width="72" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="216" y="136" width="72" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="20" y="54" width="96" height="4" rx="2" fill="#F4795B" opacity="1.0"/></svg></div>
<h3>executive_summary</h3>
<p class="hint">Synthèse SCR. Titre = So What. 3 cartes pleine largeur : situation, complication, resolution (1-2 phrases chacune).</p>
<div class="fields">
@@ -121,6 +126,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L03</span>
<span class="lmode" style="background:#061033;color:#ffffff">Sombre</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#061033" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><circle cx="300" cy="150" r="40" fill="#10204D" opacity="1.0"/><rect x="20" y="78" width="20" height="20" rx="2" fill="#F4795B" opacity="1.0"/><rect x="20" y="104" width="130" height="14" rx="2" fill="#FFFFFF" opacity="1.0"/></svg></div>
<h3>section_divider</h3>
<p class="hint">Transition de section. titre = nom de la section. Le numéro est incrémenté automatiquement par le moteur.</p>
<div class="fields">
@@ -134,6 +140,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L40</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="140" height="12" rx="2" fill="#061033" opacity="1.0"/><circle cx="32" cy="52" r="10" fill="#061033" opacity="1.0"/><rect x="50" y="47" width="180" height="9" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="230" y="47" width="60" height="9" rx="3" fill="#C9CDD3" opacity="0.6"/><circle cx="32" cy="84" r="10" fill="#F4795B" opacity="1.0"/><rect x="50" y="79" width="180" height="9" rx="3" fill="#061033" opacity="1.0"/><rect x="230" y="79" width="60" height="9" rx="3" fill="#C9CDD3" opacity="0.6"/><circle cx="32" cy="116" r="10" fill="#061033" opacity="1.0"/><rect x="50" y="111" width="180" height="9" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="230" y="111" width="60" height="9" rx="3" fill="#C9CDD3" opacity="0.6"/><circle cx="32" cy="148" r="10" fill="#061033" opacity="1.0"/><rect x="50" y="143" width="180" height="9" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="230" y="143" width="60" height="9" rx="3" fill="#C9CDD3" opacity="0.6"/></svg></div>
<h3>agenda</h3>
<p class="hint">Sommaire du deck : à placer en slide 2 pour toute présentation de plus de 15 minutes. sections = liste de {label, numero?, duree?, actif?}, 2 à 8 entrées. actif: true met la section en corail (utile pour les rappels d'agenda en cours de deck). duree (ex "10 min") s'affiche à droite en discret.</p>
<div class="fields">
@@ -155,6 +162,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L10</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="120" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="90" y="66" width="140" height="40" rx="4" fill="#F4795B" opacity="1.0"/><rect x="110" y="116" width="100" height="8" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="134" y="132" width="52" height="5" rx="2" fill="#C9CDD3" opacity="0.6"/></svg></div>
<h3>big_stat</h3>
<p class="hint">Un chiffre héro plein écran. valeur = le chiffre (ex "78%"), description = ce qu'il signifie, source = référence.</p>
<div class="fields">
@@ -168,6 +176,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L09</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="120" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="56" width="96" height="100" rx="6" fill="#F4F1EC" opacity="1.0"/><rect x="32" y="74" width="50" height="22" rx="3" fill="#F4795B" opacity="1.0"/><rect x="32" y="104" width="70" height="6" rx="3" fill="#46505A" opacity="1.0"/><rect x="32" y="118" width="56" height="6" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="112" y="56" width="96" height="100" rx="6" fill="#F4F1EC" opacity="1.0"/><rect x="124" y="74" width="50" height="22" rx="3" fill="#061033" opacity="1.0"/><rect x="124" y="104" width="70" height="6" rx="3" fill="#46505A" opacity="1.0"/><rect x="124" y="118" width="56" height="6" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="204" y="56" width="96" height="100" rx="6" fill="#F4F1EC" opacity="1.0"/><rect x="216" y="74" width="50" height="22" rx="3" fill="#061033" opacity="1.0"/><rect x="216" y="104" width="70" height="6" rx="3" fill="#46505A" opacity="1.0"/><rect x="216" y="118" width="56" height="6" rx="3" fill="#C9CDD3" opacity="1.0"/></svg></div>
<h3>kpi_grid</h3>
<p class="hint">2 à 6 cartes KPI. items = [{label, valeur, description}]. valeur en grand corail, label en en-tête, description en source.</p>
<div class="fields">
@@ -181,6 +190,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L42</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="120" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="40" y="110" width="32" height="40" rx="2" fill="#061033" opacity="1.0"/><rect x="92" y="80" width="32" height="70" rx="2" fill="#061033" opacity="1.0"/><rect x="144" y="95" width="32" height="55" rx="2" fill="#061033" opacity="1.0"/><rect x="196" y="60" width="32" height="90" rx="2" fill="#061033" opacity="1.0"/><rect x="248" y="88" width="32" height="62" rx="2" fill="#061033" opacity="1.0"/><rect x="30" y="150" width="270" height="2" rx="0" fill="#46505A" opacity="1.0"/></svg></div>
<h3>bar_chart</h3>
<p class="hint">Comparaison de valeurs par catégories — graphique NATIF éditable. Max 8 catégories × 3 séries. series = liste de {label, values} ; values = nombres SANS guillemets, alignés sur categories. horizontal: true pour des barres (libellés longs). unite (ex M€) et source optionnels. Couleurs imposées : navy, coral, glacier.</p>
<div class="fields">
@@ -194,6 +204,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L43</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="120" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="30" y="150" width="270" height="2" rx="0" fill="#46505A" opacity="1.0"/><polyline points="40,130 100,96 160,110 220,70 288,84" fill="none" stroke="#061033" stroke-width="2.5"/><circle cx="288" cy="84" r="5" fill="#F4795B" opacity="1.0"/></svg></div>
<h3>line_chart</h3>
<p class="hint">Évolution temporelle — graphique NATIF éditable. Max 12 points × 3 séries. points_x = libellés d'axe (mois, années...) ; series = {label, values}, nombres sans guillemets. Le dernier point de la première série est automatiquement mis en valeur (corail).</p>
<div class="fields">
@@ -207,6 +218,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L44</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="110" height="12" rx="2" fill="#061033" opacity="1.0"/><circle cx="100" cy="106" r="40" fill="none" stroke="#061033" stroke-width="18"/><path d="M100 66 A40 40 0 0 1 138 118" fill="none" stroke="#F4795B" stroke-width="18"/><circle cx="210" cy="84" r="6" fill="#061033" opacity="1.0"/><rect x="224" y="80" width="76" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><circle cx="210" cy="108" r="6" fill="#F4795B" opacity="1.0"/><rect x="224" y="104" width="76" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><circle cx="210" cy="132" r="6" fill="#8FA9D0" opacity="1.0"/><rect x="224" y="128" width="76" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/></svg></div>
<h3>donut_split</h3>
<p class="hint">Répartition d'un tout : anneau à gauche + légende détaillée à droite. 2 à 6 segments = {label, valeur} (nombres sans guillemets). valeur_centrale (optionnelle) s'affiche au centre de l'anneau (ex : total « 120 M€ »). Couleurs = cycle PR imposé.</p>
<div class="fields">
@@ -220,6 +232,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L45</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="140" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="30" y="150" width="270" height="2" rx="0" fill="#46505A" opacity="1.0"/><rect x="40" y="80" width="34" height="70" rx="2" fill="#061033" opacity="1.0"/><rect x="94" y="80" width="34" height="34" rx="2" fill="#F4795B" opacity="1.0"/><rect x="148" y="60" width="34" height="40" rx="2" fill="#F4795B" opacity="1.0"/><rect x="202" y="44" width="34" height="30" rx="2" fill="#F4795B" opacity="1.0"/><rect x="256" y="60" width="34" height="90" rx="2" fill="#061033" opacity="1.0"/></svg></div>
<h3>waterfall</h3>
<p class="hint">Pont de valeur (waterfall) : expliquer un écart entre deux montants par des variations successives. depart et arrivee = {label, valeur} ; marches = liste de {label, delta} SIGNÉ (positif ou négatif, max 8). Le moteur calcule les cumuls — ne jamais fournir de cumul. Idéal pour : évolution de budget, pont d'effectifs, décomposition d'un résultat.</p>
<div class="fields">
@@ -241,6 +254,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L07</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="170" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="44" width="132" height="120" rx="6" fill="#F4F1EC" opacity="1.0"/><rect x="20" y="44" width="132" height="26" rx="6" fill="#061033" opacity="1.0"/><rect x="20" y="58" width="132" height="12" rx="0" fill="#061033" opacity="1.0"/><rect x="32" y="52" width="70" height="10" rx="3" fill="#FFFFFF" opacity="1.0"/><rect x="32" y="84" width="100" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="32" y="99" width="100" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="32" y="114" width="100" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="32" y="129" width="100" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="168" y="44" width="132" height="120" rx="6" fill="#F4F1EC" opacity="1.0"/><rect x="168" y="44" width="132" height="26" rx="6" fill="#F4795B" opacity="1.0"/><rect x="168" y="58" width="132" height="12" rx="0" fill="#F4795B" opacity="1.0"/><rect x="180" y="52" width="70" height="10" rx="3" fill="#FFFFFF" opacity="1.0"/><rect x="180" y="84" width="100" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="180" y="99" width="100" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="180" y="114" width="100" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="180" y="129" width="100" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/></svg></div>
<h3>two_cols_text</h3>
<p class="hint">Deux colonnes en cartes avec en-tête coloré. left/right = {titre, bullets[]}. Gauche = navy, droite = corail.</p>
<div class="fields">
@@ -254,6 +268,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L34</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="150" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="48" width="280" height="20" rx="4" fill="#061033" opacity="1.0"/><rect x="24" y="52" width="84" height="12" rx="2" fill="#FFFFFF" opacity="0.5"/><rect x="116" y="52" width="84" height="12" rx="2" fill="#FFFFFF" opacity="0.5"/><rect x="208" y="52" width="84" height="12" rx="2" fill="#FFFFFF" opacity="0.5"/><rect x="20" y="72" width="280" height="18" rx="2" fill="#FFFFFF" opacity="1.0" stroke="#E3E0D9" stroke-width="1"/><rect x="30" y="77" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="122" y="77" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="214" y="77" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="20" y="94" width="280" height="18" rx="2" fill="#F4F1EC" opacity="1.0" stroke="#E3E0D9" stroke-width="1"/><rect x="30" y="99" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="122" y="99" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="214" y="99" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="20" y="116" width="280" height="18" rx="2" fill="#FFFFFF" opacity="1.0" stroke="#E3E0D9" stroke-width="1"/><rect x="30" y="121" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="122" y="121" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="214" y="121" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="20" y="138" width="280" height="18" rx="2" fill="#F4F1EC" opacity="1.0" stroke="#E3E0D9" stroke-width="1"/><rect x="30" y="143" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="122" y="143" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="214" y="143" width="64" height="7" rx="2" fill="#C9CDD3" opacity="1.0"/></svg></div>
<h3>comparison_table</h3>
<p class="hint">Tableau structuré multi-critères. headers = liste de colonnes (1ère = critères). rows = [{label, values: []}] ou listes plates. Max 5 colonnes, 8 lignes.</p>
<div class="fields">
@@ -267,6 +282,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L46</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="120" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="60" y="54" width="42" height="22" rx="2" fill="#D6DCEA" opacity="1.0"/><rect x="106" y="54" width="42" height="22" rx="2" fill="#9FB0D0" opacity="1.0"/><rect x="152" y="54" width="42" height="22" rx="2" fill="#8FA9D0" opacity="1.0"/><rect x="198" y="54" width="42" height="22" rx="2" fill="#10204D" opacity="1.0"/><rect x="244" y="54" width="42" height="22" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="58" width="32" height="12" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="60" y="80" width="42" height="22" rx="2" fill="#9FB0D0" opacity="1.0"/><rect x="106" y="80" width="42" height="22" rx="2" fill="#8FA9D0" opacity="1.0"/><rect x="152" y="80" width="42" height="22" rx="2" fill="#10204D" opacity="1.0"/><rect x="198" y="80" width="42" height="22" rx="2" fill="#061033" opacity="1.0"/><rect x="244" y="80" width="42" height="22" rx="2" fill="#D6DCEA" opacity="1.0"/><rect x="20" y="84" width="32" height="12" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="60" y="106" width="42" height="22" rx="2" fill="#8FA9D0" opacity="1.0"/><rect x="106" y="106" width="42" height="22" rx="2" fill="#10204D" opacity="1.0"/><rect x="152" y="106" width="42" height="22" rx="2" fill="#061033" opacity="1.0"/><rect x="198" y="106" width="42" height="22" rx="2" fill="#D6DCEA" opacity="1.0"/><rect x="244" y="106" width="42" height="22" rx="2" fill="#9FB0D0" opacity="1.0"/><rect x="20" y="110" width="32" height="12" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="60" y="132" width="42" height="22" rx="2" fill="#10204D" opacity="1.0"/><rect x="106" y="132" width="42" height="22" rx="2" fill="#061033" opacity="1.0"/><rect x="152" y="132" width="42" height="22" rx="2" fill="#D6DCEA" opacity="1.0"/><rect x="198" y="132" width="42" height="22" rx="2" fill="#9FB0D0" opacity="1.0"/><rect x="244" y="132" width="42" height="22" rx="2" fill="#8FA9D0" opacity="1.0"/><rect x="20" y="136" width="32" height="12" rx="2" fill="#C9CDD3" opacity="1.0"/></svg></div>
<h3>heatmap_table</h3>
<p class="hint">Tableau à intensité : évaluer plusieurs items sur plusieurs critères. headers = colonnes (max 6) ; rows = {label, scores} avec score ENTIER de 0 (faible) à 4 (fort), max 8 lignes. Le moteur traduit chaque score en teinte de navy — aucune autre donnée. Idéal pour : maturité, couverture fonctionnelle, cartographie de risques.</p>
<div class="fields">
@@ -288,6 +304,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L08</span>
<span class="lmode" style="background:#061033;color:#ffffff">Sombre</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#061033" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="40" y="70" width="240" height="14" rx="2" fill="#FFFFFF" opacity="1.0"/><rect x="70" y="92" width="180" height="14" rx="2" fill="#8FA9D0" opacity="1.0"/><rect x="130" y="120" width="60" height="6" rx="2" fill="#F4795B" opacity="1.0"/></svg></div>
<h3>key_message</h3>
<p class="hint">Message clé en citation plein écran. message = la phrase forte, detail = sous-texte optionnel.</p>
<div class="fields">
@@ -309,6 +326,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L17</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="130" height="12" rx="2" fill="#061033" opacity="1.0"/><circle cx="144" cy="108" r="22" fill="#061033" opacity="1.0"/><circle cx="93" cy="137" r="22" fill="#46505A" opacity="1.0"/><circle cx="93" cy="79" r="22" fill="#8FA9D0" opacity="1.0"/><circle cx="110" cy="108" r="14" fill="#F4795B" opacity="1.0"/><circle cx="210" cy="70" r="6" fill="#061033" opacity="1.0"/><rect x="224" y="66" width="76" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><circle cx="210" cy="100" r="6" fill="#46505A" opacity="1.0"/><rect x="224" y="96" width="76" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><circle cx="210" cy="130" r="6" fill="#8FA9D0" opacity="1.0"/><rect x="224" y="126" width="76" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/></svg></div>
<h3>circular_diagram</h3>
<p class="hint">3 à 6 valeurs en cercles + légende à droite. segments = [{label, description, couleur?}].</p>
<div class="fields">
@@ -322,6 +340,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L41</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="110" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="115" y="132" width="90" height="20" rx="2" fill="#F4795B" opacity="1.0"/><rect x="85" y="108" width="150" height="20" rx="2" fill="#8FA9D0" opacity="1.0"/><rect x="55" y="84" width="210" height="20" rx="2" fill="#46505A" opacity="1.0"/><rect x="25" y="60" width="270" height="20" rx="2" fill="#061033" opacity="1.0"/></svg></div>
<h3>pyramid</h3>
<p class="hint">Argumentation pyramidale : la conclusion au sommet, les fondations à la base. niveaux = liste de {label, description?}, EXACTEMENT 3 ou 4 niveaux, du sommet vers la base. Largeurs d'étages fixes gérées par le moteur. Idéal pour : message clé et ses appuis, hiérarchie stratégie/tactiques/moyens.</p>
<div class="fields">
@@ -343,6 +362,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L06</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="150" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="66" width="8" height="8" rx="1" fill="#F4795B" opacity="1.0"/><rect x="38" y="66" width="250" height="8" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="20" y="94" width="8" height="8" rx="1" fill="#F4795B" opacity="1.0"/><rect x="38" y="94" width="250" height="8" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="20" y="122" width="8" height="8" rx="1" fill="#F4795B" opacity="1.0"/><rect x="38" y="122" width="250" height="8" rx="3" fill="#C9CDD3" opacity="1.0"/></svg></div>
<h3>default_bullets</h3>
<p class="hint">Liste de points clés. Si texte au format "Mot : explication", le moteur met le mot en gras navy. Max 5 bullets. bullets = [{niveau, texte}].</p>
<div class="fields">
@@ -364,6 +384,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L19</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="150" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="52" width="280" height="28" rx="6" fill="#F4F1EC" opacity="1.0"/><circle cx="38" cy="66" r="11" fill="#061033" opacity="1.0"/><rect x="60" y="62" width="200" height="8" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="20" y="86" width="280" height="28" rx="6" fill="#F4F1EC" opacity="1.0"/><circle cx="38" cy="100" r="11" fill="#061033" opacity="1.0"/><rect x="60" y="96" width="200" height="8" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="20" y="120" width="280" height="28" rx="6" fill="#F4F1EC" opacity="1.0"/><circle cx="38" cy="134" r="11" fill="#061033" opacity="1.0"/><rect x="60" y="130" width="200" height="8" rx="3" fill="#C9CDD3" opacity="1.0"/></svg></div>
<h3>numbered_steps</h3>
<p class="hint">Étapes en cartes pleine largeur avec badge rond numéroté. steps = [{numero, titre, description}].</p>
<div class="fields">
@@ -377,6 +398,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L36</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="140" height="12" rx="2" fill="#061033" opacity="1.0"/><polygon points="20,80 90,80 106,104 90,128 20,128 36,104" fill="#061033"/><polygon points="120,80 190,80 206,104 190,128 120,128 136,104" fill="#F4795B"/><polygon points="220,80 290,80 306,104 290,128 220,128 236,104" fill="#061033"/></svg></div>
<h3>process_arrow</h3>
<p class="hint">Flux horizontal de 3 à 6 étapes. steps = [{titre, description}]. Badges numérotés corail, couleurs cycle alternées. Max 6 étapes.</p>
<div class="fields">
@@ -390,6 +412,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L47</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="110" height="12" rx="2" fill="#061033" opacity="1.0"/><polygon points="40.0,58 280.0,58 266.0,80 54.0,80" fill="#061033"/><polygon points="70.0,84 250.0,84 236.0,106 84.0,106" fill="#10204D"/><polygon points="100.0,110 220.0,110 206.0,132 114.0,132" fill="#8FA9D0"/><polygon points="125.0,136 195.0,136 181.0,158 139.0,158" fill="#F4795B"/></svg></div>
<h3>funnel</h3>
<p class="hint">Entonnoir de conversion : volumes décroissants d'étape en étape. etapes = {label, valeur, description?} du haut vers le bas, 3 à 5 étages. Largeurs proportionnelles aux valeurs (plancher de lisibilité), dernier étage corail. Idéal pour : pipeline commercial, adoption, qualification progressive.</p>
<div class="fields">
@@ -411,6 +434,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L23</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="150" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="74" width="96" height="44" rx="6" fill="#061033" opacity="1.0"/><rect x="116" y="94" width="16" height="4" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="40" y="126" width="56" height="6" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="112" y="74" width="96" height="44" rx="6" fill="#46505A" opacity="1.0"/><rect x="208" y="94" width="16" height="4" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="132" y="126" width="56" height="6" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="204" y="74" width="96" height="44" rx="6" fill="#8FA9D0" opacity="1.0"/><rect x="224" y="126" width="56" height="6" rx="3" fill="#C9CDD3" opacity="1.0"/></svg></div>
<h3>phases_timeline</h3>
<p class="hint">Phases reliées par une ligne pointillée. phases = [{label, periode}]. Couleurs du cycle theme.</p>
<div class="fields">
@@ -424,6 +448,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L32</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="140" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="56" width="60" height="10" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="130" y="56" width="120" height="12" rx="3" fill="#061033" opacity="1.0"/><rect x="20" y="82" width="60" height="10" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="180" y="82" width="90" height="12" rx="3" fill="#46505A" opacity="1.0"/><rect x="20" y="108" width="60" height="10" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="240" y="108" width="100" height="12" rx="3" fill="#061033" opacity="1.0"/><rect x="20" y="134" width="60" height="10" rx="2" fill="#C9CDD3" opacity="1.0"/><rect x="160" y="134" width="130" height="12" rx="3" fill="#8FA9D0" opacity="1.0"/></svg></div>
<h3>gantt_timeline</h3>
<p class="hint">Gantt par workstreams. periods = liste de labels (ex ["Juin","Juil"]). workstreams = [{label, tasks: [{label, start, end}]}]. start/end = index dans periods (0-based). Max 3 workstreams, 4 tâches chacun.</p>
<div class="fields">
@@ -445,6 +470,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L28</span>
<span class="lmode" style="background:#8FA9D0;color:#061033">Panel</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="150" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="48" width="190" height="116" rx="6" fill="#F4F1EC" opacity="1.0"/><rect x="20" y="48" width="190" height="6" rx="3" fill="#F4795B" opacity="1.0"/><circle cx="40" cy="72" r="10" fill="#061033" opacity="1.0"/><rect x="60" y="66" width="130" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="60" y="82" width="130" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="60" y="98" width="130" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="60" y="114" width="130" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="228" y="48" width="72" height="116" rx="6" fill="#061033" opacity="1.0"/><rect x="240" y="70" width="48" height="8" rx="3" fill="#8FA9D0" opacity="1.0"/><rect x="240" y="92" width="48" height="7" rx="3" fill="#33436A" opacity="1.0"/><rect x="240" y="105" width="48" height="7" rx="3" fill="#33436A" opacity="1.0"/><rect x="240" y="118" width="48" height="7" rx="3" fill="#33436A" opacity="1.0"/></svg></div>
<h3>recommendation_card</h3>
<p class="hint">Carte de recommandation. Sidebar = numero + titre + cta. Corps = headline + bullets.</p>
<div class="fields">
@@ -466,6 +492,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L30</span>
<span class="lmode" style="background:#061033;color:#ffffff">Sombre</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#061033" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><circle cx="300" cy="24" r="40" fill="#F4795B" opacity="0.9"/><circle cx="30" cy="150" r="46" fill="#10204D" opacity="1.0"/><rect x="96" y="74" width="128" height="14" rx="2" fill="#FFFFFF" opacity="1.0"/><rect x="120" y="100" width="80" height="8" rx="2" fill="#8FA9D0" opacity="1.0"/></svg></div>
<h3>end_slide</h3>
<p class="hint">Slide de fin. message = phrase de conclusion.</p>
<div class="fields">
@@ -487,6 +514,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L31</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="150" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="50" width="120" height="100" rx="6" fill="#F4F1EC" opacity="1.0"/><rect x="20" y="50" width="120" height="6" rx="3" fill="#061033" opacity="1.0"/><rect x="32" y="70" width="90" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="32" y="86" width="90" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="32" y="102" width="90" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="32" y="118" width="90" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="176" y="50" width="120" height="100" rx="6" fill="#F4F1EC" opacity="1.0"/><rect x="176" y="50" width="120" height="6" rx="3" fill="#46505A" opacity="1.0"/><rect x="188" y="70" width="90" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="188" y="86" width="90" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="188" y="102" width="90" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="188" y="118" width="90" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><circle cx="160" cy="100" r="10" fill="#F4795B" opacity="1.0"/></svg></div>
<h3>from_to_pairs</h3>
<p class="hint">Transformation avant/après en paires alignées. pairs = [{from, to}]. label_from/label_to = en-têtes colonnes. Max 5 paires. Idéal pour tangibiliser un changement.</p>
<div class="fields">
@@ -508,6 +536,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L33</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="130" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="100" width="280" height="3" rx="0" fill="#061033" opacity="1.0"/><circle cx="40" cy="101" r="7" fill="#061033" opacity="1.0"/><rect x="20" y="116" width="40" height="6" rx="2" fill="#C9CDD3" opacity="1.0"/><circle cx="120" cy="101" r="7" fill="#F4795B" opacity="1.0"/><rect x="100" y="116" width="40" height="6" rx="2" fill="#C9CDD3" opacity="1.0"/><circle cx="200" cy="101" r="7" fill="#061033" opacity="1.0"/><rect x="180" y="116" width="40" height="6" rx="2" fill="#C9CDD3" opacity="1.0"/><circle cx="280" cy="101" r="7" fill="#061033" opacity="1.0"/><rect x="260" y="116" width="40" height="6" rx="2" fill="#C9CDD3" opacity="1.0"/></svg></div>
<h3>yearly_timeline</h3>
<p class="hint">Frise chronologique horizontale. milestones = [{annee, label, actif?}]. actif = true pour le jalon courant (corail). Labels alternent haut/bas. Max 6 jalons.</p>
<div class="fields">
@@ -529,6 +558,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L35</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="120" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="20" y="48" width="280" height="18" rx="4" fill="#061033" opacity="1.0"/><rect x="20" y="70" width="80" height="16" rx="2" fill="#F4F1EC" opacity="1.0"/><circle cx="130" cy="78" r="7" fill="#061033" opacity="0.85"/><circle cx="174" cy="78" r="7" fill="#F4795B" opacity="0.85"/><circle cx="218" cy="78" r="7" fill="#46505A" opacity="0.85"/><circle cx="262" cy="78" r="7" fill="#8FA9D0" opacity="0.85"/><rect x="20" y="92" width="80" height="16" rx="2" fill="#F4F1EC" opacity="1.0"/><circle cx="130" cy="100" r="7" fill="#061033" opacity="0.85"/><circle cx="174" cy="100" r="7" fill="#F4795B" opacity="0.85"/><circle cx="218" cy="100" r="7" fill="#46505A" opacity="0.85"/><circle cx="262" cy="100" r="7" fill="#8FA9D0" opacity="0.85"/><rect x="20" y="114" width="80" height="16" rx="2" fill="#F4F1EC" opacity="1.0"/><circle cx="130" cy="122" r="7" fill="#061033" opacity="0.85"/><circle cx="174" cy="122" r="7" fill="#F4795B" opacity="0.85"/><circle cx="218" cy="122" r="7" fill="#46505A" opacity="0.85"/><circle cx="262" cy="122" r="7" fill="#8FA9D0" opacity="0.85"/><rect x="20" y="136" width="80" height="16" rx="2" fill="#F4F1EC" opacity="1.0"/><circle cx="130" cy="144" r="7" fill="#061033" opacity="0.85"/><circle cx="174" cy="144" r="7" fill="#F4795B" opacity="0.85"/><circle cx="218" cy="144" r="7" fill="#46505A" opacity="0.85"/><circle cx="262" cy="144" r="7" fill="#8FA9D0" opacity="0.85"/></svg></div>
<h3>raci_table</h3>
<p class="hint">Matrice RACI. roles = liste de rôles. tasks = [{label, raci: [R/A/C/I]}]. R=Responsable(corail), A=Autorité(navy), C=Consulté(slate), I=Informé(muted). Max 4 rôles, 8 tâches.</p>
<div class="fields">
@@ -550,6 +580,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L37</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="120" height="12" rx="2" fill="#061033" opacity="1.0"/><rect x="130" y="52" width="60" height="26" rx="4" fill="#061033" opacity="1.0"/><rect x="60" y="120" width="56" height="26" rx="4" fill="#F4F1EC" opacity="1.0" stroke="#E3E0D9" stroke-width="1"/><line x1="160" y1="78" x2="88" y2="120" stroke="#061033" stroke-width="1.5"/><rect x="140" y="120" width="56" height="26" rx="4" fill="#F4F1EC" opacity="1.0" stroke="#E3E0D9" stroke-width="1"/><line x1="160" y1="78" x2="168" y2="120" stroke="#061033" stroke-width="1.5"/><rect x="220" y="120" width="56" height="26" rx="4" fill="#F4F1EC" opacity="1.0" stroke="#E3E0D9" stroke-width="1"/><line x1="160" y1="78" x2="248" y2="120" stroke="#061033" stroke-width="1.5"/></svg></div>
<h3>org_chart</h3>
<p class="hint">Organigramme hiérarchique max 3 niveaux. root = {label, children: [{label, children?: [{label}]}]}. Max 4 enfants directs, 3 petits-enfants par enfant.</p>
<div class="fields">
@@ -571,6 +602,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L38</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="16" width="120" height="12" rx="2" fill="#061033" opacity="1.0"/><line x1="160" y1="46" x2="160" y2="170" stroke="#46505A" stroke-width="1.5"/><line x1="24" y1="108" x2="296" y2="108" stroke="#46505A" stroke-width="1.5"/><circle cx="92" cy="78" r="16" fill="#061033" opacity="0.9"/><circle cx="228" cy="78" r="16" fill="#F4795B" opacity="0.9"/><circle cx="92" cy="140" r="16" fill="#46505A" opacity="0.9"/><circle cx="228" cy="140" r="16" fill="#8FA9D0" opacity="0.9"/></svg></div>
<h3>matrix_2x2</h3>
<p class="hint">Matrice effort/impact. axis_x/axis_y = {label, low, high}. quadrants = {top_left, top_right, bottom_left, bottom_right}. items = [{label, x, y}] avec x/y de 0 à 100. Max 8 items.</p>
<div class="fields">
@@ -592,6 +624,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L50</span>
<span class="lmode" style="background:#f4f1ec;color:#061033">Clair</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="20" y="44" width="130" height="120" rx="4" fill="#8FA9D0" opacity="0.5"/><line x1="20" y1="164" x2="150" y2="44" stroke="#8FA9D0" stroke-width="1.5"/><line x1="20" y1="44" x2="150" y2="164" stroke="#8FA9D0" stroke-width="1.5"/><rect x="168" y="44" width="130" height="14" rx="2" fill="#061033" opacity="1.0"/><circle cx="176" cy="78" r="3" fill="#F4795B" opacity="1.0"/><rect x="184" y="74" width="108" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="184" y="91" width="108" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="184" y="108" width="108" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/><rect x="184" y="125" width="108" height="7" rx="3" fill="#C9CDD3" opacity="1.0"/></svg></div>
<h3>image_split</h3>
<p class="hint">Image d'appui sur 40 % de la slide (side: left par défaut, right possible) + titre et points clés (max 4 bullets). À utiliser quand une image du dossier assets/ du projet illustre le propos ; image = nom de fichier exact tel que listé par /lire. legende (optionnelle) s'affiche sur un bandeau navy sous l'image.</p>
<div class="fields">
@@ -605,6 +638,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
<span class="lcode">L51</span>
<span class="lmode" style="background:#061033;color:#ffffff">Sombre</span>
</div>
<div class="wf-thumb"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" class="wf"><rect x="0" y="0" width="320" height="180" rx="8" fill="#FFFFFF" opacity="1.0"/><rect x="0.5" y="0.5" width="319" height="179" rx="8" fill="none" stroke="#E3E0D9"/><rect x="0" y="0" width="320" height="180" rx="4" fill="#8FA9D0" opacity="0.5"/><line x1="0" y1="180" x2="320" y2="0" stroke="#8FA9D0" stroke-width="1.5"/><line x1="0" y1="0" x2="320" y2="180" stroke="#8FA9D0" stroke-width="1.5"/><rect x="0" y="120" width="320" height="60" rx="0" fill="#061033" opacity="0.82"/><rect x="20" y="134" width="150" height="12" rx="2" fill="#FFFFFF" opacity="1.0"/><rect x="20" y="152" width="90" height="7" rx="2" fill="#8FA9D0" opacity="1.0"/></svg></div>
<h3>image_full</h3>
<p class="hint">Ouverture de chapitre visuelle : image plein cadre + voile navy + titre display blanc. Alternative à section_divider quand un asset du projet s'y prête. image = nom de fichier exact de assets/.</p>
<div class="fields">
@@ -614,7 +648,7 @@ footer{padding:50px 60px;background:var(--navy);color:rgba(255,255,255,.6);font-
</div></div>
</section>
<footer>
Généré automatiquement depuis layouts_v2.yaml · 09/07/2026 · 31 layouts
Généré automatiquement depuis layouts_v2.yaml · 16/07/2026 · 31 layouts
</footer>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
patch_prompt_designer_meta.py — Le Designer ne conclut plus.
Bug : ses commentaires de synthèse (« Points clés de la proposition… »)
après la dernière slide fuyaient dans le deck (slide 27 du deck Sisley).
Défense en profondeur : encoder_schema v3 tronque déjà après '---' ;
cette règle prompt tarit la source.
Usage : python3 patch_prompt_designer_meta.py
puis prompt_injection_v2.py + recoller le Designer dans Mistral Studio.
"""
import shutil, sys
from pathlib import Path
BLOC = """- Ta réponse se termine à la DERNIÈRE ligne de contenu de la dernière
slide. AUCUN commentaire, bilan, synthèse ou justification après —
tout texte postérieur serait transcrit dans le deck comme du contenu.
"""
p = Path("prompt_the_designer_v3.md")
if not p.exists():
print(" ! prompt_the_designer_v3.md introuvable"); sys.exit(1)
c = p.read_text(encoding="utf-8")
if "se termine à la DERNIÈRE ligne" in c:
print(" = déjà à jour"); sys.exit(0)
anchor = "- Tu ajoutes UNIQUEMENT les lignes `@layout:` et `@note:`."
if c.count(anchor) != 1:
print(" ! ancre introuvable/non unique"); sys.exit(1)
shutil.copy2(p, str(p) + ".bak-meta")
p.write_text(c.replace(anchor, anchor + "\n" + BLOC), encoding="utf-8")
print(" + règle anti-conclusion insérée — propager avec prompt_injection_v2.py")
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
patch_render_engine_c5fin.py — Finitions C5 (2 micro-fixes visuels)
===================================================================
F1. bar_chart horizontal : PowerPoint affiche les catégories de BAS
en HAUT en BAR_CLUSTERED — la première catégorie du YAML se
retrouvait en bas. On inverse catégories ET valeurs quand
horizontal, pour un ordre de lecture naturel (haut → bas).
F2. waterfall : les libellés de catégories longs (Décommissionnements…)
wrappaient en orphelin. Réduction automatique à 9 pt au-delà de
14 caractères.
Usage (dossier du pipeline) : python3 patches/patch_render_engine_c5fin.py
Vérifie les ancres, écrit .bak-c5fin, compile, idempotent.
"""
import py_compile
import shutil
import sys
from pathlib import Path
TARGET = Path("render_engine_v2.py")
P1_OLD = """ cd = CategoryChartData()
cd.categories = cats
for s in series:
vals = [self._num(v) for v in (s.get("values") or [])][:len(cats)]
vals += [0.0] * (len(cats) - len(vals))
cd.add_series(pick(s, "label", default="Série"), tuple(vals))
x, y, w, h = self._chart_zone(d)
horiz = bool(d.get("horizontal"))
"""
P1_NEW = """ horiz = bool(d.get("horizontal"))
if horiz:
# BAR_CLUSTERED trace de bas en haut : on inverse pour un
# ordre de lecture naturel (1re catégorie du YAML en haut).
cats = cats[::-1]
cd = CategoryChartData()
cd.categories = cats
for s in series:
vals = [self._num(v) for v in (s.get("values") or [])][:len(cats)]
vals += [0.0] * (len(cats) - len(vals))
if horiz:
vals = vals[::-1]
cd.add_series(pick(s, "label", default="Série"), tuple(vals))
x, y, w, h = self._chart_zone(d)
"""
P2_OLD = """ def cat_label(i, label):
self._text(slide, zx + i * slot + 0.05, chart_b + 0.15,
slot - 0.1, 1.05, label, size=10,
color=self.C["slate"], align=PP_ALIGN.CENTER)
"""
P2_NEW = """ def cat_label(i, label):
sz = 9 if len(str(label)) > 14 else 10
self._text(slide, zx + i * slot + 0.05, chart_b + 0.15,
slot - 0.1, 1.05, label, size=sz,
color=self.C["slate"], align=PP_ALIGN.CENTER)
"""
def fail(m):
print(" ! %s" % m)
sys.exit(1)
def main():
if not TARGET.exists():
fail("%s introuvable." % TARGET)
c = TARGET.read_text(encoding="utf-8")
if "cats = cats[::-1]" in c:
fail("Déjà patché — rien à faire.")
for i, old in enumerate((P1_OLD, P2_OLD), 1):
if c.count(old) != 1:
fail("Ancre F%d introuvable ou non unique." % i)
shutil.copy2(TARGET, str(TARGET) + ".bak-c5fin")
c = c.replace(P1_OLD, P1_NEW).replace(P2_OLD, P2_NEW)
TARGET.write_text(c, encoding="utf-8")
try:
py_compile.compile(str(TARGET), doraise=True)
except py_compile.PyCompileError as e:
shutil.copy2(str(TARGET) + ".bak-c5fin", TARGET)
fail("Compilation : %s" % e)
print(" + F1 (ordre bar horizontal) + F2 (labels waterfall) OK.")
if __name__ == "__main__":
main()
+16
View File
@@ -338,11 +338,27 @@ annoté complet mis à jour, pas seulement la partie modifiée.
---
## LANGUE DU DECK (RÈGLE ABSOLUE)
Le contenu que tu reçois peut être dans n'importe quelle langue
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
reçu**, mot pour mot, y compris pour les titres, les bullets, les
labels, les légendes et les notes.
Ces instructions sont en français — c'est la langue de TON pilotage,
jamais celle du livrable. Un plan en anglais produit un deck 100 %
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
même deck.
## RÈGLES ABSOLUES
- Tu utilises UNIQUEMENT les 21 layouts du catalogue.
- Tu reprends le Markdown INTÉGRALEMENT — aucune perte de contenu.
- Tu ajoutes UNIQUEMENT les lignes `@layout:` et `@note:`.
- Ta réponse se termine à la DERNIÈRE ligne de contenu de la dernière
slide. AUCUN commentaire, bilan, synthèse ou justification après —
tout texte postérieur serait transcrit dans le deck comme du contenu.
- Tu ne produis ni YAML, ni plan séparé, ni JSON.
- `cover_split` en premier, `end_slide` en dernier, toujours.
+16
View File
@@ -101,11 +101,27 @@ annoté complet mis à jour, pas seulement la partie modifiée.
---
## LANGUE DU DECK (RÈGLE ABSOLUE)
Le contenu que tu reçois peut être dans n'importe quelle langue
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
reçu**, mot pour mot, y compris pour les titres, les bullets, les
labels, les légendes et les notes.
Ces instructions sont en français — c'est la langue de TON pilotage,
jamais celle du livrable. Un plan en anglais produit un deck 100 %
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
même deck.
## RÈGLES ABSOLUES
- Tu utilises UNIQUEMENT les 21 layouts du catalogue.
- Tu reprends le Markdown INTÉGRALEMENT — aucune perte de contenu.
- Tu ajoutes UNIQUEMENT les lignes `@layout:` et `@note:`.
- Ta réponse se termine à la DERNIÈRE ligne de contenu de la dernière
slide. AUCUN commentaire, bilan, synthèse ou justification après —
tout texte postérieur serait transcrit dans le deck comme du contenu.
- Tu ne produis ni YAML, ni plan séparé, ni JSON.
- `cover_split` en premier, `end_slide` en dernier, toujours.
+12 -7
View File
@@ -451,13 +451,6 @@ bullets:
---
### Notes du présentateur
Si une slide du plan contient une ligne « Notes : ... », transcris son
contenu dans un champ `notes` de la slide YAML (chaîne simple). Les
lignes de justification du Designer (commençant par →) ne sont NI du
contenu NI des notes : ignore-les.
## GESTION DE LA LONGUEUR
Si la présentation dépasse 8 slides, tu travailles en blocs :
@@ -469,6 +462,18 @@ Le YAML de chaque bloc doit être **syntaxiquement valide indépendamment** —
---
## LANGUE DU DECK (RÈGLE ABSOLUE)
Le contenu que tu reçois peut être dans n'importe quelle langue
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
reçu**, mot pour mot, y compris pour les titres, les bullets, les
labels, les légendes et les notes.
Ces instructions sont en français — c'est la langue de TON pilotage,
jamais celle du livrable. Un plan en anglais produit un deck 100 %
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
même deck.
## RÈGLES ABSOLUES
- Tu utilises UNIQUEMENT les 21 layouts du catalogue — aucun autre nom n'existe
+12
View File
@@ -81,6 +81,18 @@ Le YAML de chaque bloc doit être **syntaxiquement valide indépendamment** —
---
## LANGUE DU DECK (RÈGLE ABSOLUE)
Le contenu que tu reçois peut être dans n'importe quelle langue
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
reçu**, mot pour mot, y compris pour les titres, les bullets, les
labels, les légendes et les notes.
Ces instructions sont en français — c'est la langue de TON pilotage,
jamais celle du livrable. Un plan en anglais produit un deck 100 %
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
même deck.
## RÈGLES ABSOLUES
- Tu utilises UNIQUEMENT les 21 layouts du catalogue — aucun autre nom n'existe
+9 -2
View File
@@ -1711,14 +1711,20 @@ class RenderEngineV2:
if len(series) > 3:
print(f" ~ bar_chart : {len(series)} séries → 3 (borne)")
series = series[:3]
horiz = bool(d.get("horizontal"))
if horiz:
# BAR_CLUSTERED trace de bas en haut : on inverse pour un
# ordre de lecture naturel (1re catégorie du YAML en haut).
cats = cats[::-1]
cd = CategoryChartData()
cd.categories = cats
for s in series:
vals = [self._num(v) for v in (s.get("values") or [])][:len(cats)]
vals += [0.0] * (len(cats) - len(vals))
if horiz:
vals = vals[::-1]
cd.add_series(pick(s, "label", default="Série"), tuple(vals))
x, y, w, h = self._chart_zone(d)
horiz = bool(d.get("horizontal"))
ctype = (XL_CHART_TYPE.BAR_CLUSTERED if horiz
else XL_CHART_TYPE.COLUMN_CLUSTERED)
ch = self._chart_base(slide, x, y, w, h, ctype, cd)
@@ -1929,8 +1935,9 @@ class RenderEngineV2:
align=PP_ALIGN.CENTER, fit=False)
def cat_label(i, label):
sz = 9 if len(str(label)) > 14 else 10
self._text(slide, zx + i * slot + 0.05, chart_b + 0.15,
slot - 0.1, 1.05, label, size=10,
slot - 0.1, 1.05, label, size=sz,
color=self.C["slate"], align=PP_ALIGN.CENTER)
def connector(x1, x2, level):
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
# resilience_boot.sh — Résilience C2 · à planifier en tâche DSM
# « Au démarrage », utilisateur ROOT (recrée la règle sudoers).
# Rejouable sans risque (idempotent). Journalise dans resilience.log.
BASE=/volume1/homes/Master/App/Sliding/python-pptx
LOG="$BASE/resilience.log"
TS=$(date '+%Y-%m-%d %H:%M:%S')
# 1. Règle sudoers de la preview (sautée par les MAJ DSM majeures)
SUDOERS=/etc/sudoers.d/sliding-preview
if [ ! -f "$SUDOERS" ]; then
printf 'Master ALL=(root) NOPASSWD: %s/preview.sh\n' "$BASE" > "$SUDOERS"
chmod 440 "$SUDOERS"
echo "$TS [sudoers] regle sliding-preview RECREEE" >> "$LOG"
fi
# 2. Purge des previews de plus de 30 jours (le disque du DS218 est petit)
N=$(find "$BASE/projets" -type d -name "*_previews" -mtime +30 2>/dev/null | wc -l)
if [ "$N" -gt 0 ]; then
find "$BASE/projets" -type d -name "*_previews" -mtime +30 -exec rm -rf {} + 2>/dev/null
echo "$TS [purge] $N dossier(s) de previews > 30j supprimes" >> "$LOG"
fi
# 3. L'image docker de la preview a-t-elle survécu ?
DOCKER=/volume1/@appstore/ContainerManager/usr/bin/docker
if [ -x "$DOCKER" ]; then
if ! "$DOCKER" image inspect sliding-preview >/dev/null 2>&1; then
echo "$TS [docker] IMAGE sliding-preview ABSENTE — rebuild requis (voir fiche systeme Trilium)" >> "$LOG"
fi
else
echo "$TS [docker] demon/binaire docker indisponible" >> "$LOG"
fi
+3 -2
View File
@@ -4,15 +4,16 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sliding — {% block title %}{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="stylesheet" href="/static/style.css">
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body>
<header>
<a class="brand" href="/">SLIDING <span>· Pernod Ricard</span></a>
<nav><a href="/logout">Déconnexion</a></nav>
<nav><a href="/layouts" target="_blank">Catalogue des layouts</a> · <a href="/logout">Déconnexion</a></nav>
</header>
<main>{% block content %}{% endblock %}</main>
<div id="busy" class="htmx-indicator">Le NAS travaille…</div>
<div id="busy" class="htmx-indicator">L'agent travaille…</div>
</body>
</html>
+1 -1
View File
@@ -60,7 +60,7 @@
{% if previews %}
<div class="gallery">
{% for n in previews %}
<figure><img src="/p/{{ p.slug }}/previews/{{ n }}" loading="lazy">
<figure><img src="/p/{{ p.slug }}/previews/{{ n }}?v={{ cache_v }}" loading="lazy">
<figcaption>{{ n }}</figcaption></figure>
{% endfor %}
</div>
+85 -31
View File
@@ -191,15 +191,16 @@ async def project_page(request):
entry = NARRATORS.get(slug)
chat = entry["chat"] if entry else []
# previews existantes ?
previews = []
pptx = info.get("dernier_pptx")
previews, cache_v = [], 0
pptx = _pptx_path(slug)
if pptx:
prev_dir = Path(pptx).parent / f"{Path(pptx).stem}_previews"
prev_dir = pptx.parent / f"{pptx.stem}_previews"
cache_v = int(pptx.stat().st_mtime)
if prev_dir.is_dir():
previews = sorted(p.name for p in
prev_dir.glob("slide-*.png"))
return render("project.html", p=info, chat_html=_chat_html(chat),
previews=previews)
previews=previews, cache_v=cache_v)
# ── routes : chat Narrator ───────────────────────────────────────────────────
@@ -285,28 +286,45 @@ async def formalise(request):
entry = _narrator(slug)
if not entry["started"]:
return HTMLResponse('<p class="err">Aucune conversation '
'Narrator — travaille le plan d\'abord.'
'</p>')
'Narrator — travaille le plan d\'abord, '
'puis sauvegarde-le.</p>')
proj = fac.Project(slug)
try:
md = entry["agent"].send(
"formalise — produis la formalisation complète slide par "
"slide (SLIDE N — titre, contenus, Notes éventuelles), "
"sans commentaire autour.")
entry["chat"].append(("system", "Formalisation demandée…"))
# Message IDENTIQUE à /formalise du facilitator (le Narrator
# produit un Markdown structuré #/##/### — PAS des "SLIDE N —
# layout" : ce sont le Designer puis l'Encoder qui les créent).
md = entry["agent"].feedback(
"Formalise maintenant le plan compact validé en Markdown "
"structuré complet pour le Designer (format #, ##, ###), "
"en développant le contenu de chaque slide. Inclus les "
"slides tiroir en les marquant [OPT-XXX].")
entry["chat"].append(("system", "Formalisation demandée."))
entry["chat"].append(("narrator", md))
# Garde : le Narrator a-t-il vraiment produit un plan ?
if md.count("#") < 3 or len(md) < 300:
return HTMLResponse(
'<p class="err">Le Narrator n\'a pas renvoyé un plan '
'formalisé exploitable (réponse trop courte ou non '
'structurée). Vérifie le plan dans le chat, ajuste, '
'puis relance.</p>')
proj.mark_formalised(md)
r = api.formalise(slug, md)
except Exception as e:
return HTMLResponse(f'<p class="err">Échec : '
f'{html.escape(str(e))}</p>')
if r.get("error"):
return HTMLResponse(f'<p class="err">'
f'{html.escape(str(r))}</p>')
return HTMLResponse(
f'<p class="err">{html.escape(str(r.get("error")))}'
f'<br><span class="hint">Le Designer/Encoder n\'a pas su '
f'transcrire ce plan. Regarde la formalisation dans le '
f'chat : elle doit lister les slides une à une.</span>'
f'</p>')
return HTMLResponse(
f'<p class="ok">Deck généré : {r["slides"]} slides — '
f'{html.escape(r["validation"])}<br>'
f'<a href="/p/{slug}/download">Télécharger le PPTX</a> · '
f'<button hx-post="/p/{slug}/preview" '
f'hx-target="#gallery" hx-indicator="#busy">Générer les '
f'aperçus</button></p>')
f'<a class="btn" href="/p/{slug}/download">Télécharger le '
f'PPTX</a> <span class="hint">puis « Générer les aperçus » '
f'ci-dessus pour les voir.</span></p>')
async def preview(request):
@@ -325,6 +343,8 @@ async def preview(request):
f'</figcaption></figure>'
for i, n in enumerate(r["slides"]))
return HTMLResponse(
f'<p class="ok">{len(r["slides"])} aperçus générés. Coche des '
f'slides pour les réviser.</p>'
f'<form hx-post="/p/{slug}/revise" hx-target="#pipeline-out" '
f'hx-indicator="#busy"><div class="gallery">{cells}</div>'
f'<input name="instruction" placeholder="Instruction de '
@@ -349,33 +369,45 @@ async def revise(request):
f'{html.escape(str(r))}</p>')
return HTMLResponse(
f'<p class="ok">Slides {r["slides_revisees"]} révisées, deck '
f'complet régénéré.<br><a href="/p/{slug}/download">'
f'Télécharger</a> · <button hx-post="/p/{slug}/preview" '
f'hx-target="#gallery" hx-indicator="#busy">Re-générer les '
f'aperçus</button></p>')
f'complet régénéré.<br><a class="btn" href="/p/{slug}/'
f'download">Télécharger</a> <span class="hint">puis '
f'« Générer les aperçus » pour revoir le deck.</span></p>')
# ── routes : fichiers ────────────────────────────────────────────────────────
def _pptx_path(slug):
"""Chemin ABSOLU du dernier PPTX. project_state.json stocke un
chemin relatif à la racine du pipeline la webapp peut avoir un
cwd différent, on résout donc depuis BASE."""
proj = fac.Project(slug)
raw = (proj.load_state().get("dernier_pptx") or "").strip()
if not raw:
return None
p = Path(raw)
if not p.is_absolute():
p = BASE / p
return p if p.exists() else None
async def preview_png(request):
guard = require_login(request)
if guard:
return guard
slug = request.path_params["slug"]
name = request.path_params["name"]
proj = fac.Project(slug)
state = proj.load_state()
pptx = state.get("dernier_pptx")
pptx = _pptx_path(slug)
if not pptx:
return Response(status_code=404)
prev_dir = Path(pptx).parent / f"{Path(pptx).stem}_previews"
prev_dir = pptx.parent / f"{pptx.stem}_previews"
try:
png = _safe_child(prev_dir, name)
except ValueError:
return Response(status_code=403)
if not png.exists():
return Response(status_code=404)
return FileResponse(str(png), media_type="image/png")
return FileResponse(str(png), media_type="image/png",
headers={"Cache-Control": "no-store"})
async def download(request):
@@ -383,11 +415,32 @@ async def download(request):
if guard:
return guard
slug = request.path_params["slug"]
proj = fac.Project(slug)
pptx = proj.load_state().get("dernier_pptx")
if not pptx or not Path(pptx).exists():
return Response(status_code=404)
return FileResponse(pptx, filename=Path(pptx).name)
pptx = _pptx_path(slug)
if not pptx:
return HTMLResponse(
'<p class="err">Aucun PPTX disponible pour ce projet — '
'lance « Formaliser le deck » d\'abord.</p>',
status_code=404)
return FileResponse(
str(pptx), filename=pptx.name,
media_type="application/vnd.openxmlformats-officedocument."
"presentationml.presentation")
async def layouts_gallery(request):
"""Catalogue des layouts — l'index.html généré par build_gallery.py,
servi tel quel (toujours à jour après une régénération)."""
guard = require_login(request)
if guard:
return guard
page = BASE / "index.html"
if not page.exists():
return HTMLResponse(
'<p style="font-family:sans-serif;margin:40px">Galerie '
'absente — lancer <code>python3 build_gallery.py</code> '
'sur le NAS.</p>', status_code=404)
return FileResponse(str(page), media_type="text/html",
headers={"Cache-Control": "no-store"})
async def health(request):
@@ -411,6 +464,7 @@ app = Starlette(routes=[
Route("/p/{slug}/revise", revise, methods=["POST"]),
Route("/p/{slug}/previews/{name}", preview_png, methods=["GET"]),
Route("/p/{slug}/download", download, methods=["GET"]),
Route("/layouts", layouts_gallery, methods=["GET"]),
Route("/health", health, methods=["GET"]),
Mount("/static", StaticFiles(directory=str(BASE / "static")),
name="static"),
+498
View File
@@ -0,0 +1,498 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
wireframes.py Vignettes SVG des layouts (Sliding Design System)
=================================================================
Schémas grosse maille, aux couleurs de la charte, FIDÈLES à la
structure de chaque _render_* du moteur (cartes, bandeaux navy/coral,
cercles, colonnes, barres, badges). Pur Python/SVG : aucune
dépendance, aucun LibreOffice, régénérable en une fraction de seconde.
API : svg_for(layout_name, mode) -> str (SVG inline, viewBox 320x180).
Un layout inconnu retombe sur un gabarit générique titre + lignes.
Consommé par build_gallery.py (vignette dans chaque fiche).
"""
# ── Charte ───────────────────────────────────────────────────────────────────
NAVY = "#061033"
NAVY2 = "#10204D"
CORAL = "#F4795B"
GLACIER = "#8FA9D0"
SLATE = "#46505A"
CARD = "#F4F1EC"
BODY = "#2B3440"
MUTED = "#C9CDD3"
WHITE = "#FFFFFF"
W, H = 320, 180
MX = 20 # marge horizontale de la zone utile
# ── Primitives ───────────────────────────────────────────────────────────────
def _r(x, y, w, h, fill, rad=0, op=1.0, stroke=None):
s = f' stroke="{stroke}" stroke-width="1"' if stroke else ""
return (f'<rect x="{x:.0f}" y="{y:.0f}" width="{w:.0f}" '
f'height="{h:.0f}" rx="{rad}" fill="{fill}" '
f'opacity="{op}"{s}/>')
def _c(cx, cy, rad, fill, op=1.0):
return (f'<circle cx="{cx:.0f}" cy="{cy:.0f}" r="{rad:.0f}" '
f'fill="{fill}" opacity="{op}"/>')
def _title_bar(y=16, w=150, c=NAVY):
return _r(MX, y, w, 12, c, rad=2)
def _lines(x, y, w, n, gap=13, c=MUTED, first=None, fw=None):
out = []
for i in range(n):
cc = first if (first and i == 0) else c
ww = (fw or w) if (fw and i == 0) else w
out.append(_r(x, y + i * gap, ww, 7, cc, rad=3))
return "".join(out)
def _img_zone(x, y, w, h, mode="light"):
"""Zone image : aplat glacier clair + diagonale (convention wireframe)."""
fill = GLACIER
return (_r(x, y, w, h, fill, rad=4, op=0.5)
+ f'<line x1="{x:.0f}" y1="{y+h:.0f}" x2="{x+w:.0f}" '
f'y2="{y:.0f}" stroke="{GLACIER}" stroke-width="1.5"/>'
+ f'<line x1="{x:.0f}" y1="{y:.0f}" x2="{x+w:.0f}" '
f'y2="{y+h:.0f}" stroke="{GLACIER}" stroke-width="1.5"/>')
def _frame(mode, body):
bg = NAVY if mode == "dark" else ("#EEF1F6" if mode == "panel"
else WHITE)
return (f'<svg xmlns="http://www.w3.org/2000/svg" '
f'viewBox="0 0 {W} {H}" class="wf">'
f'{_r(0, 0, W, H, bg, rad=8)}'
f'<rect x="0.5" y="0.5" width="{W-1}" height="{H-1}" rx="8" '
f'fill="none" stroke="#E3E0D9"/>{body}</svg>')
def _title_light(mode):
return WHITE if mode == "dark" else NAVY
# ── Archétypes (fidèles aux _render_*) ───────────────────────────────────────
def _wf_cover(mode):
# cover_split : fond navy, cercles décoratifs coin, filet coral, titre
b = [_c(300, 20, 46, NAVY2), _c(288, 150, 30, CORAL, 0.9),
_c(250, 150, 14, GLACIER),
_r(MX, 74, 40, 4, CORAL, rad=1),
_r(MX, 88, 150, 16, WHITE, rad=2),
_r(MX, 120, 96, 8, GLACIER, rad=2)]
return _frame("dark", "".join(b))
def _wf_section(mode):
# section_divider : navy, gros numéro fantôme, titre
b = [_c(300, 150, 40, NAVY2),
_r(MX, 78, 20, 20, CORAL, rad=2),
_r(MX, 104, 130, 14, WHITE, rad=2)]
return _frame("dark", "".join(b))
def _wf_end(mode):
b = [_c(300, 24, 40, CORAL, 0.9), _c(30, 150, 46, NAVY2),
_r(96, 74, 128, 14, WHITE, rad=2),
_r(120, 100, 80, 8, GLACIER, rad=2)]
return _frame("dark", "".join(b))
def _wf_big_stat(mode):
tl = _title_light(mode)
# titre en haut ; chiffre coral CENTRÉ (H et V) ; desc + source
# centrées dessous — cf _render_big_stat (align CENTER, _cy).
b = [_title_bar(w=120, c=tl),
_r(90, 66, 140, 40, CORAL, rad=4), # chiffre héros, centré
_r(110, 116, 100, 8, MUTED, rad=3), # description centrée
_r(134, 132, 52, 5, MUTED, rad=2, op=0.6)] # source centrée
return _frame(mode, "".join(b))
def _wf_key_message(mode):
# key_message : fond navy, grande phrase centrée
b = [_r(40, 70, 240, 14, WHITE, rad=2),
_r(70, 92, 180, 14, GLACIER, rad=2),
_r(130, 120, 60, 6, CORAL, rad=2)]
return _frame("dark", "".join(b))
def _wf_bullets(mode):
tl = _title_light(mode)
# titre en haut ; puces CARRÉES coral + texte, bloc centré vertical.
# Pas de barre latérale (cf _render_default_bullets).
b = [_title_bar(c=tl)]
for y in (66, 94, 122):
b.append(_r(MX, y, 8, 8, CORAL, rad=1)) # square_mark coral
b.append(_r(MX + 18, y, 250, 8, MUTED, rad=3))
return _frame(mode, "".join(b))
def _wf_two_cols(mode):
b = [_title_bar(w=170)]
cw, xs, acc = 132, (MX, 168), (NAVY, CORAL)
for i in range(2):
x = xs[i]
b.append(_r(x, 44, cw, 120, CARD, rad=6))
b.append(_r(x, 44, cw, 26, acc[i], rad=6))
b.append(_r(x, 58, cw, 12, acc[i]))
b.append(_r(x + 12, 52, 70, 10, WHITE, rad=3))
b.append(_lines(x + 12, 84, 100, 4, gap=15))
return _frame("light", "".join(b))
def _wf_from_to(mode):
b = [_title_bar(w=150)]
for i, (x, c) in enumerate(((MX, NAVY), (176, SLATE))):
b.append(_r(x, 50, 120, 100, CARD, rad=6))
b.append(_r(x, 50, 120, 6, c, rad=3))
b.append(_lines(x + 12, 70, 90, 4, gap=16))
b.append(_c(160, 100, 10, CORAL)) # flèche/pivot central
return _frame("light", "".join(b))
def _wf_exec_summary(mode):
b = [_title_bar(w=160)]
for i, x in enumerate((MX, 112, 204)):
b.append(_r(x, 54, 96, 108, CARD, rad=6))
b.append(_c(x + 16, 74, 9, NAVY))
b.append(_r(x + 12, 96, 72, 7, SLATE, rad=3))
b.append(_lines(x + 12, 112, 72, 3, gap=12))
b.append(_r(MX, 54, 96, 4, CORAL, rad=2)) # 1re carte accentuée
return _frame("light", "".join(b))
def _wf_kpi_grid(mode):
b = [_title_bar(w=120)]
xs = (MX, 112, 204)
for i, x in enumerate(xs):
b.append(_r(x, 56, 96, 100, CARD, rad=6))
b.append(_r(x + 12, 74, 50, 22, CORAL if i == 0 else NAVY,
rad=3))
b.append(_r(x + 12, 104, 70, 6, SLATE, rad=3))
b.append(_r(x + 12, 118, 56, 6, MUTED, rad=3))
return _frame("light", "".join(b))
def _wf_numbered_steps(mode):
b = [_title_bar(w=150)]
for i, y in enumerate((52, 86, 120)):
b.append(_r(MX, y, 280, 28, CARD, rad=6))
b.append(_c(MX + 18, y + 14, 11, NAVY))
b.append(_r(MX + 40, y + 10, 200, 8, MUTED, rad=3))
return _frame("light", "".join(b))
def _wf_agenda(mode):
b = [_title_bar(w=140)]
for i, y in enumerate((52, 84, 116, 148)):
active = (i == 1)
b.append(_c(MX + 12, y, 10, CORAL if active else NAVY))
b.append(_r(MX + 30, y - 5, 180, 9,
NAVY if active else MUTED, rad=3))
b.append(_r(230, y - 5, 60, 9, MUTED, rad=3, op=0.6))
return _frame("light", "".join(b))
def _wf_circular(mode):
tl = _title_light(mode)
b = [_title_bar(w=130, c=tl)]
cx, cy = 110, 108
for ang, c in ((0, NAVY), (120, SLATE), (240, GLACIER)):
import math
x = cx + 34 * math.cos(math.radians(ang))
y = cy + 34 * math.sin(math.radians(ang))
b.append(_c(x, y, 22, c))
b.append(_c(cx, cy, 14, CORAL))
# légende
for i, y in enumerate((70, 100, 130)):
b.append(_c(210, y, 6, (NAVY, SLATE, GLACIER)[i]))
b.append(_r(224, y - 4, 76, 7, MUTED, rad=3))
return _frame(mode, "".join(b))
def _wf_recommendation(mode):
b = [_title_bar(w=150),
_r(MX, 48, 190, 116, CARD, rad=6),
_r(MX, 48, 190, 6, CORAL, rad=3),
_c(MX + 20, 72, 10, NAVY),
_lines(MX + 40, 66, 130, 4, gap=16),
_r(228, 48, 72, 116, NAVY, rad=6), # panneau latéral navy
_r(240, 70, 48, 8, GLACIER, rad=3),
_lines(240, 92, 48, 3, gap=13, c="#33436A")]
return _frame("light", "".join(b))
def _wf_phases_timeline(mode):
b = [_title_bar(w=150)]
for i, x in enumerate((MX, 112, 204)):
c = (NAVY, SLATE, GLACIER)[i]
b.append(_r(x, 74, 96, 44, c, rad=6))
if i < 2:
b.append(_r(x + 96, 94, 16, 4, MUTED, rad=2))
b.append(_r(x + 20, 126, 56, 6, MUTED, rad=3))
return _frame("light", "".join(b))
def _wf_gantt(mode):
b = [_title_bar(w=140)]
rows = ((40, 120, NAVY), (90, 90, SLATE), (150, 100, NAVY),
(70, 130, GLACIER))
for i, (x, w, c) in enumerate(rows):
y = 56 + i * 26
b.append(_r(MX, y, 60, 10, MUTED, rad=2)) # label
b.append(_r(90 + x, y, w, 12, c, rad=3)) # barre
return _frame("light", "".join(b))
def _wf_yearly(mode):
b = [_title_bar(w=130),
_r(MX, 100, 280, 3, NAVY)] # ligne de temps
for i, x in enumerate((40, 120, 200, 280)):
b.append(_c(x, 101, 7, CORAL if i == 1 else NAVY))
b.append(_r(x - 20, 116, 40, 6, MUTED, rad=2))
return _frame("light", "".join(b))
def _wf_comparison(mode):
b = [_title_bar(w=150), _r(MX, 48, 280, 20, NAVY, rad=4)] # entête
for j in range(3):
b.append(_r(MX + 4 + j * 92, 52, 84, 12, WHITE, rad=2,
op=0.5))
for i in range(4):
y = 72 + i * 22
b.append(_r(MX, y, 280, 18, CARD if i % 2 else WHITE, rad=2,
stroke="#E3E0D9"))
for j in range(3):
b.append(_r(MX + 10 + j * 92, y + 5, 64, 7, MUTED, rad=2))
return _frame("light", "".join(b))
def _wf_raci(mode):
b = [_title_bar(w=120), _r(MX, 48, 280, 18, NAVY, rad=4)]
letters = (NAVY, CORAL, SLATE, GLACIER)
for i in range(4):
y = 70 + i * 22
b.append(_r(MX, y, 80, 16, CARD, rad=2))
for j in range(4):
b.append(_c(130 + j * 44, y + 8, 7, letters[j], 0.85))
return _frame("light", "".join(b))
def _wf_process_arrow(mode):
b = [_title_bar(w=140)]
for i, x in enumerate((MX, 120, 220)):
c = CORAL if i == 1 else NAVY
b.append(f'<polygon points="{x},80 {x+70},80 {x+86},104 '
f'{x+70},128 {x},128 {x+16},104" fill="{c}"/>')
return _frame("light", "".join(b))
def _wf_org_chart(mode):
b = [_title_bar(w=120),
_r(130, 52, 60, 26, NAVY, rad=4)]
for x in (60, 140, 220):
b.append(_r(x, 120, 56, 26, CARD, rad=4, stroke="#E3E0D9"))
b.append(f'<line x1="160" y1="78" x2="{x+28}" y2="120" '
f'stroke="{NAVY}" stroke-width="1.5"/>')
return _frame("light", "".join(b))
def _wf_matrix(mode):
tl = _title_light(mode)
b = [_title_bar(w=120, c=tl),
f'<line x1="160" y1="46" x2="160" y2="170" stroke="{SLATE}" '
f'stroke-width="1.5"/>',
f'<line x1="24" y1="108" x2="296" y2="108" stroke="{SLATE}" '
f'stroke-width="1.5"/>']
quad = ((92, 78, NAVY), (228, 78, CORAL),
(92, 140, SLATE), (228, 140, GLACIER))
for cx, cy, c in quad:
b.append(_c(cx, cy, 16, c, 0.9))
return _frame(mode, "".join(b))
def _wf_image_split(mode):
b = [_img_zone(MX, 44, 130, 120),
_r(168, 44, 130, 14, NAVY, rad=2),
_c(176, 78, 3, CORAL),
_lines(184, 74, 108, 4, gap=17)]
return _frame("light", "".join(b))
def _wf_image_full(mode):
b = [_img_zone(0, 0, W, H),
_r(0, 120, W, 60, NAVY, rad=0, op=0.82),
_r(MX, 134, 150, 12, WHITE, rad=2),
_r(MX, 152, 90, 7, GLACIER, rad=2)]
return _frame("light", "".join(b))
def _wf_bar_chart(mode):
b = [_title_bar(w=120)]
vals = (40, 70, 55, 90, 62)
for i, v in enumerate(vals):
x = 40 + i * 52
b.append(_r(x, 150 - v, 32, v, NAVY, rad=2))
b.append(_r(30, 150, 270, 2, SLATE))
return _frame("light", "".join(b))
def _wf_line_chart(mode):
b = [_title_bar(w=120), _r(30, 150, 270, 2, SLATE)]
pts = [(40, 130), (100, 96), (160, 110), (220, 70), (288, 84)]
path = " ".join(f"{x},{y}" for x, y in pts)
b.append(f'<polyline points="{path}" fill="none" stroke="{NAVY}" '
f'stroke-width="2.5"/>')
b.append(_c(288, 84, 5, CORAL))
return _frame("light", "".join(b))
def _wf_donut(mode):
tl = _title_light(mode)
b = [_title_bar(w=110, c=tl)]
b.append(f'<circle cx="100" cy="106" r="40" fill="none" '
f'stroke="{NAVY}" stroke-width="18"/>')
b.append(f'<path d="M100 66 A40 40 0 0 1 138 118" fill="none" '
f'stroke="{CORAL}" stroke-width="18"/>')
for i, y in enumerate((84, 108, 132)):
b.append(_c(210, y, 6, (NAVY, CORAL, GLACIER)[i]))
b.append(_r(224, y - 4, 76, 7, MUTED, rad=3))
return _frame(mode, "".join(b))
def _wf_waterfall(mode):
b = [_title_bar(w=140), _r(30, 150, 270, 2, SLATE)]
# colonnes cumulées : base navy, deltas coral, total navy
cols = ((40, 150 - 70, 70, NAVY), (94, 80, 34, CORAL),
(148, 60, 40, CORAL), (202, 44, 30, CORAL),
(256, 150 - 90, 90, NAVY))
for x, y, h, c in cols:
b.append(_r(x, y, 34, h, c, rad=2))
return _frame("light", "".join(b))
def _wf_heatmap(mode):
b = [_title_bar(w=120)]
import random
random.seed(3)
shades = ["#D6DCEA", "#9FB0D0", GLACIER, NAVY2, NAVY]
for r in range(4):
for cix in range(5):
b.append(_r(60 + cix * 46, 54 + r * 26, 42, 22,
shades[(r + cix) % 5], rad=2))
b.append(_r(MX, 58 + r * 26, 32, 12, MUTED, rad=2))
return _frame("light", "".join(b))
def _wf_funnel(mode):
b = [_title_bar(w=110)]
widths = (240, 180, 120, 70)
cols = (NAVY, NAVY2, GLACIER, CORAL)
for i, w in enumerate(widths):
x = (W - w) / 2
b.append(f'<polygon points="{x},{58+i*26} {x+w},{58+i*26} '
f'{x+w-14},{80+i*26} {x+14},{80+i*26}" '
f'fill="{cols[i]}"/>')
return _frame("light", "".join(b))
def _wf_pyramid(mode):
b = [_title_bar(w=110)]
widths = (90, 150, 210, 270)
cols = (CORAL, GLACIER, SLATE, NAVY)
for i, w in enumerate(widths):
x = (W - w) / 2
b.append(_r(x, 132 - i * 24, w, 20, cols[i], rad=2))
return _frame("light", "".join(b))
def _wf_generic(mode):
tl = _title_light(mode)
return _frame(mode, _title_bar(c=tl) + _lines(MX, 50, 240, 5,
gap=18))
# ── Mapping layout → archétype ───────────────────────────────────────────────
_MAP = {
"cover_split": _wf_cover,
"section_divider": _wf_section,
"end_slide": _wf_end,
"big_stat": _wf_big_stat,
"key_message": _wf_key_message,
"default_bullets": _wf_bullets,
"two_cols_text": _wf_two_cols,
"from_to_pairs": _wf_from_to,
"executive_summary": _wf_exec_summary,
"kpi_grid": _wf_kpi_grid,
"numbered_steps": _wf_numbered_steps,
"agenda": _wf_agenda,
"circular_diagram": _wf_circular,
"recommendation_card": _wf_recommendation,
"phases_timeline": _wf_phases_timeline,
"gantt_timeline": _wf_gantt,
"yearly_timeline": _wf_yearly,
"comparison_table": _wf_comparison,
"raci_table": _wf_raci,
"process_arrow": _wf_process_arrow,
"org_chart": _wf_org_chart,
"matrix_2x2": _wf_matrix,
"image_split": _wf_image_split,
"image_full": _wf_image_full,
"bar_chart": _wf_bar_chart,
"line_chart": _wf_line_chart,
"donut_split": _wf_donut,
"waterfall": _wf_waterfall,
"heatmap_table": _wf_heatmap,
"funnel": _wf_funnel,
"pyramid": _wf_pyramid,
# freeform : pas de structure fixe → gabarit générique
"freeform": _wf_generic,
}
def svg_for(layout_name, mode="light"):
fn = _MAP.get(layout_name, _wf_generic)
try:
return fn(mode)
except Exception:
return _wf_generic(mode)
def known():
return set(_MAP)
if __name__ == "__main__":
# Auto-test : rend tous les archétypes dans un HTML de contrôle
import yaml
import os
try:
L = yaml.safe_load(open("layouts_v2.yaml",
encoding="utf-8"))["layouts"]
names = list(L)
except Exception:
names = list(_MAP)
cells = "".join(
f'<figure style="margin:8px;display:inline-block;width:240px">'
f'{svg_for(n)}<figcaption style="font:12px sans-serif;'
f'text-align:center">{n}</figcaption></figure>'
for n in names)
open("_wireframes_preview.html", "w").write(
f"<!doctype html><meta charset=utf-8><body "
f"style='background:#FAF9F7'>{cells}</body>")
missing = [n for n in names if n not in _MAP]
print("layouts:", len(names), "| mappés:",
len([n for n in names if n in _MAP]),
"| génériques:", missing or "aucun")
print("Contrôle visuel : _wireframes_preview.html")