Ajoute icône/largeur/arrondi/gras/majuscules à l'étiquette (badge)

Nouveaux attributs par élément (svg_markup/width/border_radius/bold/
uppercase, tous vides ou False par défaut = comportement historique
inchangé), réglables depuis le panneau Propriétés — même esprit que
bold/align/color déjà présents sur titre/paragraphe. Fixer une largeur
implique toujours de sortir de l'étirement pleine largeur par défaut
(align-self:flex-start posé automatiquement avec elle).

Le kicker "Module obligatoire" du thème Sécurité Incendie s'en sert
maintenant (icône flamme, largeur au contenu, arrondi complet, gras,
majuscules) pour être conforme à la maquette d'origine — la puce
décorative CSS générique qui la remplaçait disparaît du thème.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-24 11:36:30 +02:00
co-authored by Claude Sonnet 5
parent 1296edc2f8
commit e03bea39c5
9 changed files with 194 additions and 26 deletions
+14 -1
View File
@@ -81,7 +81,20 @@ def element_default_attributes(kind: str) -> dict[str, Any]:
# attribut "ordered" redondant à tenir synchronisé avec le kind.
return {"items": ["Premier élément", "Deuxième élément"]}
if kind == "badge":
return {"content": "Étiquette"}
# Même esprit que titre/paragraphe (bold/align/color... déjà des
# attributs par élément, pas des choix figés par le moteur) :
# svg_markup/width/border_radius/bold/uppercase vides ou False par
# défaut = comportement historique inchangé (pleine largeur, sans
# icône, casse normale) ; un thème ou le créateur les règle au cas
# par cas (retour utilisateur du 24/09/2026).
return {
"content": "Étiquette",
"svg_markup": "",
"width": "",
"border_radius": "",
"bold": False,
"uppercase": False,
}
if kind == "carte":
# Contenu pur (lettre/repère court + titre + description) — sans
# aucun choix de couleur/forme, laissé au futur système de
+17 -8
View File
@@ -11,12 +11,18 @@ top-niveau ou enfants d'une rangée. `"liste_puces"`/`"liste_numerotee"`
partagent la même structure d'attributs (`items`) ; c'est le `kind`
lui-même qui décide `<ul>` ou `<ol>` au rendu (voir
`document_engine/rendering/render_document_element.py::_render_list`),
jamais un attribut `ordered` redondant. `"badge"` et `"carte"` sont du
contenu PUR, sans aucun attribut de style/couleur/forme (décision du
24/09/2026 : le moteur ne porte que contenu et mécanisme, tout habillage
visuel revient à un futur système de templates) — voir aussi le mode SVG
inline de `"image"` (`svg_markup`) et la pièce jointe téléchargeable de
`"bouton"` (`attachment_filename`), ci-dessous.
jamais un attribut `ordered` redondant. `"carte"` reste du contenu PUR,
sans aucun attribut de style/couleur/forme (décision du 24/09/2026 : le
moteur ne porte que contenu et mécanisme, tout habillage visuel revient
à un futur système de templates). `"badge"` a depuis gagné des
attributs de mise en forme PAR ÉLÉMENT (`svg_markup`/`width`/
`border_radius`/`bold`/`uppercase`, retour utilisateur du 24/09/2026)
— même esprit que `bold`/`align`/`color` déjà présents sur
`"titre"`/`"paragraphe"` : des réglages posés par le créateur ou un
thème au cas par cas, jamais une valeur figée dans le moteur. Voir
aussi le mode SVG inline de `"image"` (`svg_markup`) et la pièce
jointe téléchargeable de `"bouton"` (`attachment_filename`),
ci-dessous.
## `MINIGAME_KINDS: tuple[str, ...]`
`("quiz", "association", "memory", "mots", "scenario", "zones")` —
@@ -47,8 +53,11 @@ Attributs posés à la création d'un élément de ce type (voir
rendu, voir rendering.md), bouton (`label/target/
attachment_stored_name/attachment_filename` — la pièce jointe est
indépendante de `target`, réservé à la navigation), liste à
puces/numérotée (`items`, une liste de chaînes), badge (`content`),
carte (`label/title/description`, contenu pur sans couleur/forme),
puces/numérotée (`items`, une liste de chaînes), badge
(`content/svg_markup/width/border_radius/bold/uppercase` — tous vides
ou `False` par défaut = comportement historique inchangé, voir
rendering.md), carte (`label/title/description`, contenu pur sans
couleur/forme),
rangée (`gap/align/justify`), quiz (`DEFAULT_QUIZ_CONFIG`, voir
`quiz_config.py`), association (`DEFAULT_ASSOCIATION_CONFIG`, voir
`association_config.py`), memory (`DEFAULT_MEMORY_CONFIG`, voir
@@ -3,6 +3,8 @@ import json
import random
from typing import Any
from .sanitize_svg_markup import sanitize_svg_markup
def render_document(elements: list[dict[str, Any]]) -> str:
"""Assemble le document ENTIER à partir de la liste à plat renvoyée
@@ -79,8 +81,6 @@ def _render_image(el: dict[str, Any], _children_by_parent: dict[int | None, list
# element_kind_labels.element_default_attributes). Nettoyé à
# CHAQUE rendu (jamais seulement à l'écriture) par sanitize_svg_markup,
# même défense en profondeur que html.escape sur les autres kinds.
from .sanitize_svg_markup import sanitize_svg_markup
sanitized = sanitize_svg_markup(svg_markup)
return f'<div class="docImage" data-element-id="{el["id"]}" data-kind="image">{sanitized}</div>'
src = html_lib.escape(str(a.get("src", "")))
@@ -128,8 +128,31 @@ def _render_button(el: dict[str, Any], _children_by_parent: dict[int | None, lis
def _render_badge(el: dict[str, Any], _children_by_parent: dict[int | None, list[dict[str, Any]]]) -> str:
content = html_lib.escape(str(el["attributes"].get("content", "")))
return f'<div class="docBadge" data-element-id="{el["id"]}" data-kind="badge">{content}</div>'
a = el["attributes"]
content = html_lib.escape(str(a.get("content", "")))
svg_markup = str(a.get("svg_markup", "")).strip()
icon_html = f'<span class="docBadgeIcon">{sanitize_svg_markup(svg_markup)}</span>' if svg_markup else ""
style_parts = []
width = str(a.get("width", "")).strip()
if width:
# Fixer une largeur implique de ne plus s'étirer sur toute la
# largeur de .docPageContent (comportement par défaut d'un enfant
# flex en colonne, voir static/document/document-editor.css) —
# les deux vont toujours ensemble, jamais l'un sans l'autre.
style_parts.append(f"align-self:flex-start; width:{html_lib.escape(width)};")
border_radius = str(a.get("border_radius", "")).strip()
if border_radius:
style_parts.append(f"border-radius:{html_lib.escape(border_radius)};")
if a.get("bold"):
style_parts.append("font-weight:800;")
if a.get("uppercase"):
style_parts.append("text-transform:uppercase;")
style_attr = f' style="{" ".join(style_parts)}"' if style_parts else ""
return (
f'<div class="docBadge" data-element-id="{el["id"]}" data-kind="badge"{style_attr}>{icon_html}{content}</div>'
)
def _render_carte(el: dict[str, Any], _children_by_parent: dict[int | None, list[dict[str, Any]]]) -> str:
+12 -1
View File
@@ -50,7 +50,18 @@ regroupement à chaque appel.
partir de `data-element-id` + `FORGE_DOCUMENT.slug`, même principe que
le reste des appels AJAX de l'éditeur).
- **Étiquette** (`badge`) : `<div>` portant `attributes["content"]`
échappé — contenu pur, aucun attribut de style.
échappé, précédé d'un `<span class="docBadgeIcon">` optionnel si
`svg_markup` est non vide (nettoyé par `sanitize_svg_markup`, même
mécanisme que le mode SVG de `"image"`). `width` (non vide) ajoute
`align-self:flex-start; width:{valeur};` en style inline — fixer une
largeur implique TOUJOURS de sortir de l'étirement pleine largeur par
défaut d'un enfant flex en colonne (voir static/document/
document-editor.css, `.docPageContent`), jamais l'un sans l'autre.
`border_radius` (non vide) ajoute `border-radius:{valeur};`. `bold`/
`uppercase` ajoutent respectivement `font-weight:800;`/
`text-transform:uppercase;` quand `True`. Tous ces attributs sont vides
ou `False` par défaut (comportement historique inchangé, aucun style
inline ajouté).
- **Carte** (`carte`) : `<div>` composé de trois blocs enfants
(`label`/`title`/`description`, tous échappés) — contenu pur, aucune
couleur/forme choisie ici (voir `element_kind_labels.md`).
@@ -14,7 +14,21 @@ SECURITE_INCENDIE_SEED_PAGES: list[dict[str, Any]] = [
{
"vertical_align": "center",
"blocks": [
{"kind": "badge", "attributes": {"content": "Module obligatoire"}},
{
"kind": "badge",
"attributes": {
"content": "Module obligatoire",
"svg_markup": (
'<svg viewBox="0 0 24 24" fill="currentColor">'
'<path d="M12 2C9 6 6 9 6 13a6 6 0 0 0 12 0c0-2-1-3.5-2-5 '
'.3 2-.7 3-1.5 2.3C15.5 9 15 6 12 2z"/></svg>'
),
"width": "fit-content",
"border_radius": "999px",
"bold": True,
"uppercase": True,
},
},
{"kind": "titre", "attributes": {"content": "Formation Sécurité Incendie", "style": "titre1"}},
{
"kind": "paragraphe",