feat: freeform palier 4 - derives de tokens, shapes, alpha, rotation, full_bleed (C6)
This commit is contained in:
+83
-15
@@ -1143,11 +1143,19 @@ def list_assets(proj: "Project"):
|
||||
VALID_TOKENS = {"navy", "navy_light", "coral", "glacier", "slate",
|
||||
"card", "white", "body", "muted"}
|
||||
VALID_BLOCK_TYPES = {"title", "heading", "text", "stat", "circle",
|
||||
"badge", "card", "rect", "line", "image"}
|
||||
"badge", "card", "rect", "line", "image", "shape"}
|
||||
VALID_FREE_SHAPES = {"chevron", "arrow", "triangle", "pill", "donut",
|
||||
"bracket_left", "bracket_right", "oval", "diamond",
|
||||
"hexagon", "parallelogram", "moon"}
|
||||
FREE_TOKEN_RE = re.compile(
|
||||
r"^(navy|navy_light|coral|glacier|slate|card|card_alt|white|body|muted)"
|
||||
r"(@\d{1,3})?$")
|
||||
|
||||
|
||||
def validate_freeform(raw: str):
|
||||
"""Valide la structure d'un YAML freeform. Retourne (ok, message, data)."""
|
||||
"""Valide un YAML freeform — palier 4 (C6), deux niveaux.
|
||||
Retourne (ok, message, data). Les avertissements n'empêchent pas
|
||||
le rendu ; ils sont intégrés au message."""
|
||||
try:
|
||||
data = yaml.safe_load(extract_yaml(raw))
|
||||
except yaml.YAMLError as e:
|
||||
@@ -1157,35 +1165,95 @@ def validate_freeform(raw: str):
|
||||
slides = data["slides"]
|
||||
if not slides:
|
||||
return False, "Aucune slide.", None
|
||||
errors = []
|
||||
|
||||
def color_ok(c):
|
||||
return bool(FREE_TOKEN_RE.match(str(c).strip().lower()))
|
||||
|
||||
errors, warns = [], []
|
||||
for i, s in enumerate(slides, 1):
|
||||
if s.get("layout") != "freeform":
|
||||
errors.append(f"Slide {i} : layout doit être 'freeform'")
|
||||
continue
|
||||
if s.get("mode") not in ("light", "dark"):
|
||||
errors.append(f"Slide {i} : mode doit être 'light' ou 'dark'")
|
||||
if s.get("mode") not in ("light", "dark", None):
|
||||
errors.append(f"Slide {i} : mode doit être 'light' ou "
|
||||
f"'dark'")
|
||||
bg = s.get("background")
|
||||
if bg and not color_ok(bg):
|
||||
errors.append(f"Slide {i} : background '{bg}' hors charte "
|
||||
f"(token ou token@NN)")
|
||||
blocks = s.get("blocks", [])
|
||||
if not blocks:
|
||||
errors.append(f"Slide {i} : aucun bloc")
|
||||
if len(blocks) > 8:
|
||||
errors.append(f"Slide {i} : {len(blocks)} blocs (max 8)")
|
||||
if len(blocks) > 15:
|
||||
errors.append(f"Slide {i} : {len(blocks)} blocs (max 15)")
|
||||
elif len(blocks) > 10:
|
||||
warns.append(f"Slide {i} : {len(blocks)} blocs — pense "
|
||||
f"respiration (10 max conseillé)")
|
||||
for j, b in enumerate(blocks, 1):
|
||||
loc = f"Slide {i} bloc {j}"
|
||||
bt = (b.get("type") or "text").lower()
|
||||
if bt not in VALID_BLOCK_TYPES:
|
||||
errors.append(f"Slide {i} bloc {j} : type '{bt}' invalide")
|
||||
errors.append(f"{loc} : type '{bt}' invalide")
|
||||
if bt == "shape":
|
||||
sk = str(b.get("shape") or "").lower()
|
||||
if sk and sk not in VALID_FREE_SHAPES:
|
||||
errors.append(
|
||||
f"{loc} : shape '{sk}' inconnue "
|
||||
f"({', '.join(sorted(VALID_FREE_SHAPES))})")
|
||||
col = float(b.get("col", 0)); w = float(b.get("w", 4))
|
||||
row = float(b.get("row", 0)); h = float(b.get("h", 1))
|
||||
if col + w > 12.01:
|
||||
errors.append(f"Slide {i} bloc {j} : déborde (col+w={col+w:.1f}>12)")
|
||||
errors.append(f"{loc} : déborde "
|
||||
f"(col+w={col + w:.1f}>12)")
|
||||
if row + h > 12.01:
|
||||
errors.append(f"Slide {i} bloc {j} : déborde (row+h={row+h:.1f}>12)")
|
||||
color = b.get("color")
|
||||
if color and color not in VALID_TOKENS:
|
||||
errors.append(f"Slide {i} bloc {j} : couleur '{color}' hors charte")
|
||||
errors.append(f"{loc} : déborde "
|
||||
f"(row+h={row + h:.1f}>12)")
|
||||
for key in ("color", "text_color"):
|
||||
c = b.get(key)
|
||||
if c and not color_ok(c):
|
||||
errors.append(f"{loc} : {key} '{c}' hors charte "
|
||||
f"(token ou token@NN, jamais de hex)")
|
||||
border = b.get("border")
|
||||
if border is not None:
|
||||
if not isinstance(border, dict):
|
||||
errors.append(f"{loc} : border doit être "
|
||||
f"{{color, weight}}")
|
||||
elif border.get("color") and not color_ok(border["color"]):
|
||||
errors.append(f"{loc} : border.color "
|
||||
f"'{border['color']}' hors charte")
|
||||
alpha = b.get("alpha")
|
||||
if alpha is not None:
|
||||
try:
|
||||
if not 0 <= float(alpha) <= 100:
|
||||
warns.append(f"{loc} : alpha {alpha} hors "
|
||||
f"0-100 — sera clampé")
|
||||
except (TypeError, ValueError):
|
||||
errors.append(f"{loc} : alpha '{alpha}' non "
|
||||
f"numérique")
|
||||
rot = b.get("rotation")
|
||||
if rot is not None:
|
||||
try:
|
||||
if float(rot) % 15 != 0:
|
||||
warns.append(f"{loc} : rotation {rot}° — sera "
|
||||
f"arrondie au pas de 15°")
|
||||
except (TypeError, ValueError):
|
||||
errors.append(f"{loc} : rotation '{rot}' non "
|
||||
f"numérique")
|
||||
radius = b.get("radius")
|
||||
if radius is not None:
|
||||
try:
|
||||
if not 0 <= float(radius) <= 0.5:
|
||||
warns.append(f"{loc} : radius {radius} hors "
|
||||
f"0-0.5 — sera clampé")
|
||||
except (TypeError, ValueError):
|
||||
errors.append(f"{loc} : radius '{radius}' non "
|
||||
f"numérique")
|
||||
if errors:
|
||||
return False, "\n ".join(errors), data
|
||||
return True, f"{len(slides)} slides freeform valides.", data
|
||||
|
||||
msg = f"{len(slides)} slides freeform valides."
|
||||
if warns:
|
||||
msg += "\n ⚠ " + "\n ⚠ ".join(warns)
|
||||
return True, msg, data
|
||||
|
||||
def run_free_designer(session: AgentSession, markdown: str, proj: Project):
|
||||
"""Flux libre : le Free Designer produit directement le YAML freeform."""
|
||||
|
||||
Reference in New Issue
Block a user