Audit complet de mise en forme — Bouton (3e élément du tableau)

Implémente toutes les options manquantes identifiées pour l'élément
Bouton : typographie (gras/italique/transformation/police/taille/
espacement des lettres/couleur), icône SVG optionnelle avec position
(avant/après le texte) et taille réglables — au-delà de l'Étiquette,
qui n'a qu'une icône fixe —, tous les attributs de boîte partagés déjà
créés pour Titre/Paragraphe/Image, et un effet visuel universel au
survol/clic.

Deux ajouts transversaux demandés pendant le test : un réglage "les 4
côtés de la bordure à la fois" dans le module de boîte partagé
(bénéficie automatiquement à tous les éléments qui l'utilisent), et le
glisser-déposer d'un élément du canevas vers une autre page via le
panneau Pages (le mécanisme serveur existait déjà pour la pagination
automatique, seule l'interaction manuelle manquait).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-26 12:47:46 +02:00
co-authored by Claude Sonnet 5
parent 9ad50c58b8
commit 8f879f6321
8 changed files with 529 additions and 16 deletions
@@ -222,6 +222,51 @@ def _render_list(el: dict[str, Any], _children_by_parent: dict[int | None, list[
return f'<{tag} class="docList" data-element-id="{el["id"]}" data-kind="{el["kind"]}">{items_html}</{tag}>'
_BUTTON_TEXT_TRANSFORMS = ("uppercase", "lowercase", "capitalize")
def _button_text_style(a: dict[str, Any]) -> list[str]:
"""Déclarations de typographie propres au bouton (jamais dans
box_style.py, partagé avec d'autres kinds qui n'ont pas tous une
notion de texte)."""
parts = []
if a.get("bold"):
# 800 (jamais 700, déjà le poids par défaut du CSS de base) :
# "gras" ne fait que RENFORCER le poids existant, jamais
# l'affaiblir — aucun bouton déjà créé ne change d'apparence tant
# que cette case n'est pas cochée explicitement.
parts.append("font-weight:800;")
if a.get("italic"):
parts.append("font-style:italic;")
text_transform = str(a.get("text_transform", "none"))
if text_transform in _BUTTON_TEXT_TRANSFORMS:
parts.append(f"text-transform:{text_transform};")
font_family = str(a.get("font_family", "")).strip()
if font_family:
parts.append(f"font-family:{html_lib.escape(font_family)};")
font_size = str(a.get("font_size", "")).strip()
if font_size:
parts.append(f"font-size:{html_lib.escape(font_size)};")
letter_spacing = str(a.get("letter_spacing", "")).strip()
if letter_spacing:
parts.append(f"letter-spacing:{html_lib.escape(letter_spacing)};")
text_color = str(a.get("text_color", "")).strip()
if text_color:
parts.append(f"color:{html_lib.escape(text_color)};")
return parts
def _button_icon_html(a: dict[str, Any]) -> str:
svg_markup = str(a.get("svg_markup", "")).strip()
if not svg_markup:
return ""
icon_size = str(a.get("icon_size", "")).strip()
size_style = (
f' style="width:{html_lib.escape(icon_size)}; height:{html_lib.escape(icon_size)};"' if icon_size else ""
)
return f'<span class="docButtonIcon"{size_style}>{sanitize_svg_markup(svg_markup)}</span>'
def _render_button(el: dict[str, Any], _children_by_parent: dict[int | None, list[dict[str, Any]]]) -> str:
a = el["attributes"]
label = html_lib.escape(str(a.get("label", "Bouton")))
@@ -236,9 +281,19 @@ def _render_button(el: dict[str, Any], _children_by_parent: dict[int | None, lis
# que le reste des appels AJAX de l'éditeur.
attachment_filename = html_lib.escape(str(a.get("attachment_filename", "")))
attachment_attr = f' data-attachment-filename="{attachment_filename}"' if attachment_filename else ""
style = " ".join(_button_text_style(a) + [render_box_style(a)]).strip()
style_attr = f' style="{style}"' if style else ""
icon_html = _button_icon_html(a)
label_span = f'<span class="docButtonLabel">{label}</span>'
inner = (
f"{label_span}{icon_html}" if str(a.get("icon_position", "before")) == "after" else f"{icon_html}{label_span}"
)
return (
f'<button type="button" class="docButton" data-element-id="{el["id"]}" '
f'data-kind="bouton"{target_attr}{attachment_attr}>{label}</button>'
f'data-kind="bouton"{target_attr}{attachment_attr}{style_attr}>{inner}</button>'
)
+19 -2
View File
@@ -67,14 +67,31 @@ regroupement à chaque appel.
forgeDocOpenImageLightbox), même principe que les mini-jeux et la
pièce jointe d'un bouton. `caption` (non vide) enveloppe le tout dans
un `<figure><figcaption>` échappée.
- **Bouton** : `<button>` avec son `label`, un `data-target` optionnel
- **Bouton** : `<button>` avec son `label` (enveloppé dans
`<span class="docButtonLabel">`), un `data-target` optionnel
(navigation) et un `data-attachment-filename` optionnel — marqueur
mécanique posé quand un fichier a été joint (voir
`routes/document/document_element_upload_attachment.py`), jamais
l'URL de téléchargement elle-même (ce renderer ne connaît pas le slug
du support ; `static/document/js/document-editor.js` l'assemble à
partir de `data-element-id` + `FORGE_DOCUMENT.slug`, même principe que
le reste des appels AJAX de l'éditeur).
le reste des appels AJAX de l'éditeur). Style inline (audit du
26/09/2026, réglages manquants — colonne "Bouton") : `bold` pousse
`font-weight` à `800` (jamais en dessous du `700` déjà posé par le CSS
de base — "gras" ne fait que renforcer, jamais affaiblir, aucune
régression visuelle sur les boutons déjà créés), `italic`/
`text_transform` (`"uppercase"`/`"lowercase"`/`"capitalize"`, jamais
`"none"`)/`font_family`/`font_size`/`letter_spacing`/`text_color`
(vides par défaut) + les attributs de boîte partagés
(`render_box_style`, voir `box_style.py`). `svg_markup` (optionnel,
nettoyé par `sanitize_svg_markup`) ajoute un `<span
class="docButtonIcon">` avant OU après `.docButtonLabel` selon
`icon_position` (`"before"` par défaut), dimensionné par `icon_size`
(vide = `1em`, suit la taille du texte). États interactifs
survol/actif : effet CSS universel (`filter`/`transform`, voir
`static/document/document-editor.css`), jamais configurable par
attribut — pas de notion d'état "désactivé" pour un bouton de contenu
(ce n'est pas un vrai contrôle de formulaire).
- **Étiquette** (`badge`) : `<div>` portant `attributes["content"]`
échappé, précédé d'un `<span class="docBadgeIcon">` optionnel si
`svg_markup` est non vide (nettoyé par `sanitize_svg_markup`, même