Implémente toutes les options manquantes identifiées pour les listes :
typographie complète, style/position/couleur/taille de puce (validés
selon le kind), puce personnalisée en SVG pour les listes à puces,
padding uniforme par élément (nouveau, il n'y en avait aucun), espacement
entre éléments réglable, et tous les attributs de boîte partagés sur la
liste entière. Bordure/fond/padding par élément individuel et sous-listes
imbriquées volontairement différés (portée actée avec l'utilisateur
avant implémentation : transformeraient le stockage des éléments en
objets structurés, chantier bien plus lourd).
Trois ajouts transversaux bénéficiant à plusieurs éléments : sections
"Contenu"/"Conteneur" dans tous les panneaux de propriétés, alignement
vertical du contenu dans son bloc (Titre/Paragraphe/Liste/Image
légendée), et une option pour retirer un thème appliqué ("Aucun modèle"
dans la modale, avec une nouvelle fonction db.remove_document_theme).
Quatre bugs réels trouvés et corrigés en chaîne pendant la validation
avec le thème "Sécurité incendie" : un badge de thème s'affichait
au-dessus du texte au lieu d'à côté ; le correctif a d'abord fait
disparaître les puces/numéros natifs de TOUTES les listes (bug plus
grave que celui corrigé) ; puis un marqueur natif redondant apparaissait
à côté du badge du thème ; puis une règle CSS site-large de spécificité
supérieure empêchait silencieusement ce dernier correctif. Chaque étape
vérifiée par navigateur automatisé sur un support jetable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
36 KiB
Audit des options de mise en forme — état des lieux et suivi
Document de travail (retour utilisateur du 26/09/2026) : liste tout ce qui manque en CSS, élément par élément, et sert de suivi pendant l'implémentation (un élément à la fois, dans l'ordre du tableau — implémentation → test utilisateur → validation → audit final → commit → élément suivant).
Méthode de travail (rappel)
- Un élément du tableau à la fois, dans l'ordre.
- J'implémente tout ce qui manque pour cet élément.
- Je préviens l'utilisateur, qui teste et valide/invalide.
- Dernier audit sur l'élément pour vérifier que rien n'est oublié.
- Si tout est ok : commit, puis élément suivant.
Règle transversale (retour utilisateur) : jamais de champ de texte
libre pour une valeur CSS — uniquement des boutons, des curseurs, des
listes déroulantes, des sélecteurs de couleur natifs. Les valeurs
numériques optionnelles utilisent un curseur + une case "activer"
(un <input type="range"> ne peut pas représenter "aucune valeur").
Tableau d'audit (état au 26/09/2026, avant implémentation)
| Élément | Mise en forme possible aujourd'hui | Ce qui manque (audit CSS complet) |
|---|---|---|
Titre / Paragraphe (titre/paragraphe) |
Gras, italique, souligné, couleur du texte, alignement horizontal du texte, largeur maximale | Typo : barré, surligné (overline), police de caractère, taille de police (actuellement figée par le style titre1/titre2/paragraphe/légende), espacement des lettres/mots, hauteur de ligne, transformation (majuscules/minuscules/capitales), indentation de la 1ʳᵉ ligne, ombre portée du texte, retour à la ligne (white-space), troncature avec "…" (text-overflow), sens d'écriture (RTL)Boîte : padding, margin (y compris négatif/auto pour centrer), hauteur, min/max-height, min-width Bordure/ombre : style (pointillé/tireté/double…), épaisseur, couleur, par côté, arrondi par coin, ombre portée du bloc ( box-shadow)Fond : couleur de fond, dégradé, image de fond, opacité du bloc Position/affichage : position du bloc dans son conteneur (pas juste le texte dedans), overflow, z-indexEffets : transition/animation au survol, curseur, filtre (flou, contraste…) Responsive : aucune valeur ne peut différer entre Bureau/Tablette/Mobile |
Image (image) |
URL, texte alternatif, SVG inline | Dimensionnement : largeur/hauteur explicites, min/max, ratio (aspect-ratio), object-fit (cover/contain/fill), object-positionBordure/ombre : style/couleur/épaisseur/par côté, arrondi par coin (actuellement fixe à 10px, non réglable), ombre portée Effets : filtre CSS (niveaux de gris, sépia, luminosité, flou), mix-blend-mode, découpe (clip-path, ex. cercle/hexagone), overlay couleur/dégradé au survolComportement : lien cliquable, ouverture en plein écran/lightbox au clic, chargement différé ( loading=lazy), légende (caption) affichée sous l'imagePosition : alignement horizontal dans son conteneur, margin, padding autourResponsive : image différente ou recadrage différent par taille d'écran |
Bouton (bouton) |
Texte, cible (URL/ancre), fichier joint | Typo : police, taille, gras/italique, transformation (majuscules), espacement des lettres Boîte : padding, margin, largeur/hauteur, alignement du bloc Bordure/ombre : style/couleur/épaisseur/par côté, arrondi par coin, ombre portée Fond : couleur, dégradé, image États interactifs : styles distincts survol/actif/désactivé (aucune notion d'état n'existe) Icône : aucune icône à côté du texte du bouton (contrairement au badge) Effets : transition au survol, curseur |
Liste à puces / numérotée (liste_puces/liste_numerotee) |
Contenu des éléments uniquement | Typo : tout (police, taille, gras/italique/souligné, couleur, interligne) Puces/numéros : style de puce ( list-style-type), image de puce personnalisée, position (intérieure/extérieure), couleur/taille des puces indépendante du texteBoîte : padding/margin globaux ET par élément de liste, espacement entre éléments réglable, indentation Bordure/fond : par élément de liste ou sur la liste entière Listes imbriquées : aucune notion de sous-liste |
Étiquette (badge) |
Icône SVG, largeur, arrondi (uniforme), gras, majuscules | Typo : italique, souligné, barré, police, taille de police, couleur du texte, espacement des lettres Boîte : hauteur, padding, margin, min/max-width Bordure : style (actuellement toujours plein), couleur, épaisseur, par côté (demandé explicitement), arrondi par coin (actuellement un seul rayon pour les 4 coins) Fond : couleur (actuellement figée par le thème), dégradé Ombre : box-shadowPosition de l'icône : avant/après le texte, taille de l'icône réglable |
Carte (carte) |
Contenu uniquement (repère/titre/description) | Tout : typo, boîte, bordure, fond, ombre, dimensionnement — rien n'est réglable |
Rangée (row, conteneur flex) |
gap, justify-content, align-items |
Boîte : padding, margin, largeur/hauteur explicites avec poignées de redimensionnement Bordure/fond/ombre : rien Flex avancé : flex-wrap (retour à la ligne), flex-direction (actuellement toujours en ligne, jamais en colonne), align-content, ordre des enfants, flex-grow/flex-basis par enfant (chacun a la même part aujourd'hui)Position : overflow, position (sticky/absolute), z-indexResponsive : passage automatique en colonne sous un seuil de largeur |
Mini-jeux (quiz/association/memory/mots/scenario/zones) |
theme_color (accent) + contenu propre à chaque jeu |
Tout le reste : bordure, fond, dimensions, police, espacement, ombre — entièrement figés par le CSS du thème, aucun réglage par instance |
Catégories transversales oubliées, valables pour tous les éléments :
opacity (transparence du bloc), cursor, transitions/animations CSS,
styles conditionnels par état (survol/focus/actif/désactivé), et le
responsive (aucun réglage ne peut varier entre Bureau/Tablette/Mobile
alors que ce sélecteur existe déjà dans l'éditeur).
Suivi par élément
1. Titre / Paragraphe — ✅ audité et validé (commité)
Implémenté : gras/italique/souligné/barré, majuscules/minuscules/ capitales, police (liste déroulante de polices web-safe), taille de police, hauteur de ligne, espacement des lettres, ombre du texte (3 préréglages), couleur du texte (sélecteur natif), largeur maximale, padding, margin, couleur de fond (+ bouton "Transparent"), arrondi des bords, hauteur/hauteur min/hauteur max/largeur min, ombre portée du bloc (3 préréglages), opacité, position du bloc (gauche/centré/droite/ pleine largeur), bordure par côté (style/épaisseur/couleur, indépendants sur les 4 côtés).
Mécanisme partagé créé pour l'occasion : document_engine/rendering/ box_style.py (render_box_style, default_border, BOX_DEFAULTS) —
réutilisé tel quel par tous les éléments suivants du tableau.
Volontairement laissé de côté (rarement utile pour du contenu de
formation, à ajouter si besoin) : surlignage (overline), indentation de
1ʳᵉ ligne, white-space/troncature "…", sens d'écriture RTL, dégradé/
image de fond, overflow/z-index/position absolue, transitions au
survol, réglages différents par taille d'écran (responsive), arrondi
par coin (un seul rayon pour les 4 coins, comme l'étiquette).
Changement rétroactif (au moment de l'Image, ci-dessous) : width
(largeur fixe) a été ajouté à box_style.py partagé — Titre/Paragraphe
gagne donc aussi ce réglage a posteriori (en plus de la largeur MAXIMALE
déjà là), sans repasser par une validation dédiée puisque c'est un ajout
pur (aucun comportement existant modifié).
Bugs transversaux trouvés et corrigés pendant ce chantier (concernent TOUT l'éditeur, pas seulement Titre/Paragraphe) :
- Panneau Propriétés jamais reconstruit après un clic (boutons/segments
ne reflétaient leur nouvel état qu'après rechargement de la page) —
corrigé dans
forgeDocUpdateAttributes, point d'entrée unique de toute mise à jour d'attribut. - Champs
.docField input(padding/bordure/fond/largeur 100%) hérités à tort par les cases à cocher et curseurs, cassant leur apparence native.
2. Image — ✅ audité et validé (commité)
Implémenté : légende (caption, enveloppe dans <figure>/<figcaption>),
ajustement dans son cadre (object_fit : taille réelle/couvrir/
contenir/étirer), format/ratio (aspect_ratio : libre/carré/4:3/16:9),
filtre (3 préréglages : noir et blanc/sépia/flouté), chargement différé
(loading="lazy"), comportement au clic mutuellement exclusif (aucun /
lien externe dans un nouvel onglet / plein écran — un vrai overlay
plein écran côté client, forgeDocOpenImageLightbox), tous les
attributs de boîte partagés (dont la nouvelle largeur fixe, ajoutée
à box_style.py à cette occasion).
Comme pour les mini-jeux et la pièce jointe d'un bouton, le lien et le plein écran ne sont réellement cliquables qu'en Mode Aperçu — en édition, le clic sélectionne l'élément (la navigation native du lien est bloquée pour ne pas quitter l'éditeur par accident).
Ajout (retour utilisateur : "il manque la possibilité d'uploader une
image") : téléversement d'un fichier depuis l'ordinateur (PNG/JPG/
GIF/WEBP/SVG), en plus du champ URL externe déjà là — mirroir exact du
mécanisme déjà en place pour la pièce jointe d'un bouton :
routes/document/document_element_upload_image.py (stocke sous
uploads/, jamais attachments/ — pas de as_attachment, l'image doit
s'afficher, pas se télécharger) et routes/document/ document_uploaded_file.py (route de service dédiée). Le fichier
téléversé vide svg_markup au passage (les deux modes ne coexistent
jamais). Le panneau propose maintenant les deux : sélecteur de fichier
(natif, pas de champ texte) en premier, champ URL externe en second
pour une image déjà hébergée ailleurs.
Volontairement laissé de côté (complexité/valeur douteuse pour du
contenu de formation) : mix-blend-mode, découpe clip-path, overlay
au survol, arrondi par coin, réglages responsive par taille d'écran.
Bug pré-existant corrigé au passage : docImageSrc/docImageAlt
n'étaient pas échappés avant insertion dans l'attribut value du
panneau Propriétés (mineur, mais corrigé puisque cette fonction était
déjà réécrite).
Bug réel corrigé (retour utilisateur : "la position de bloc ne
fonctionne pas sur l'image") : render_box_style (padding/margin/
fond/bordure/largeur/position du bloc, dont align_self) était
appliqué à l'<img>/<div> INTERNE, jamais à son enveloppe
(<figure>/<a class="docImageLink">/<div class= "docImageLightboxTrigger">) quand une légende ou un comportement au
clic en ajoutait une — align-self posé sur un simple descendant du
flex-item n'a aucun effet CSS, d'où le bouton "position du bloc" sans
effet visible dès qu'une légende ou un lien était configuré. Corrigé
dans _render_image (document_engine/rendering/ render_document_element.py) : le style de bloc cible désormais
toujours l'élément réellement top-niveau (enfant direct de
.docPageContent), quel que soit l'emboîtement. Changement de
comportement réel sur du contenu existant : une image avec légende/lien
et un fond/une bordure/un padding déjà réglés les verra désormais
appliqués à TOUT le bloc (image + légende), pas seulement à l'image —
c'est le comportement correct/attendu, mais je le signale car ça change
le rendu visuel d'éléments déjà créés.
Bug réel corrigé (retour utilisateur : "quand j'ajoute une image ça
crée des pages à l'infini") : une photo importée à sa taille native
pouvait dépasser une page entière à elle seule ; la pagination
automatique (forgeDocCheckPageOverflow, static/document/js/
document-editor.js) la déplaçait alors sans fin vers une page neuve,
qui débordait identiquement. Corrigé une première fois par un garde-fou générique (si tout le
contenu de la page déborde déjà, rien à répartir, on arrête) + une
hauteur maximale par défaut sur img.docImage (plafonnée à la hauteur
intérieure de la page, static/document/document-editor.css).
Affiné ensuite (retour utilisateur : "si l'image uploadée est trop
grande je préfère qu'elle soit redimensionnée plutôt que bouger sur une
autre page") : forgeDocCheckPageOverflow (static/document/js/
document-editor.js) évite de paginer une image qui déborde quand il
reste assez de place pour un résultat encore utilisable — elle est
rétrécie SUR PLACE (max-height posé en style inline,
calculé à partir de l'espace réellement disponible sous elle sur SA
page actuelle, légende comprise) à chaque rafraîchissement du canevas.
Purement visuel, jamais persisté en attribut (l'espace disponible
dépend du contenu au-dessus, qui change en éditant). Un réglage
explicite de hauteur maximale via le panneau reste prioritaire (une
valeur inline posée par un attribut serait recalculée par-dessus à
l'affichage suivant si elle déborde encore). Le garde-fou anti-boucle
et le défaut CSS restent en place pour les AUTRES kinds et comme filet
de sécurité au tout premier rendu (avant que le JS n'ait tourné).
Changement de conception (retour utilisateur : "ce cadre ne devrait
pas changer de taille en fonction de la taille de l'image mais être
fixe et contraindre l'image dedans") : plutôt que de rétrécir
dynamiquement une image trop grande selon l'espace disponible (approche
fragile, source des deux bugs ci-dessus), un nouvel élément image a
maintenant un cadre FIXE dès sa création — object_fit="cover" +
height="220px" par défaut au lieu de vides (document_engine/labels/ element_kind_labels.py) — une photo importée est donc TOUJOURS rognée
pour remplir ce cadre, quelle que soit sa résolution native. "Taille
réelle" reste un choix explicite possible dans le panneau (segmented
"Ajustement dans son cadre"). Ne s'applique qu'aux NOUVEAUX éléments
image — un élément déjà créé avant ce changement garde ses attributs
object_fit/height existants (jamais re-migré automatiquement,
sanitize_element_attributes ne touche pas au kind "image", voir sa
docstring) ; pour en faire bénéficier une image déjà présente, régler
manuellement "Couvrir" + une hauteur via le panneau. Le rétrécissement
dynamique (garde-fou anti-boucle + forgeDocCheckPageOverflow appelé
après upload/mise à jour d'attribut) reste en place comme filet de
sécurité pour une hauteur explicite déraisonnablement grande.
Bug réel corrigé une seconde fois (retour utilisateur : "ce n'est pas
redimensionner") : le rétrécissement ci-dessus vit dans
forgeDocCheckPageOverflow, appelée uniquement par
forgeDocRefreshCanvas (ajout/déplacement/suppression/Annuler-
Rétablir) — mais téléverser un fichier dans une image existante
(forgeDocApiUploadImage) et changer un attribut quelconque
(forgeDocUpdateAttributes, le point d'entrée central de tous les
panneaux) patchent chacun le DOM directement, SANS jamais passer par
ce chemin : le rétrécissement ne se déclenchait donc jamais après un
upload. Corrigé en appelant explicitement forgeDocCheckPageOverflow()
à la fin de ces deux fonctions. Pas de test automatisé possible ici :
aucune suite de tests n'existe pour document-editor.js
(package.json ne couvre que static/game/js/), vérification
manuelle uniquement.
Affiné une troisième fois (retour utilisateur : "l'ajout de page
quand le contenu déborde dois rester et même être plus intelligent, la
nouvelle page dois etre sous la page qui deborde meme si ya d'autre
page") : la pagination automatique (pour tout kind, pas seulement les
images désormais épargnées ci-dessus) insérait toujours la page neuve
en toute fin de la bande d'onglets (add_document_page l'ajoute
toujours à la fin), même si d'autres pages existaient déjà après celle
qui déborde — déborder sur la page 2 d'un support qui en compte 5
ajoutait la nouvelle page en position 6 au lieu de 3. Corrigé dans
forgeDocCheckPageOverflow : la page est créée puis immédiatement
déplacée (forgeDocApiPageMove, mécanisme déjà existant pour le
glisser-déposer du panneau Pages) juste après la page active, décalant
les pages suivantes d'un cran. Extraction d'un helper partagé
forgeDocReorderLocalPage (état client après un déplacement) réutilisé
par le glisser-déposer ET par ce nouveau cas, pour ne pas dupliquer ce
calcul. Vérification côté serveur déjà couverte par les tests existants
de move_document_page/document_page_move (l'insertion "au milieu"
de la bande d'onglets y est déjà testée) ; le déclenchement côté client
reste manuel faute de suite de tests JS.
Affiné une quatrième fois : sous un seuil de place restante
(FORGE_DOC_MIN_IMAGE_HEIGHT, 60px) il n'y a plus de place RÉELLE sur
la page (pas seulement pour cette image) — l'image bascule alors dans
la pagination normale ci-dessus au lieu d'être rétrécie à une taille
inutilisable, ce qui empêchait sinon une page pleine d'images
d'enchaîner sur une nouvelle page. Vérifié par un test de bout en bout
en conditions réelles (navigateur automatisé Playwright contre le
serveur local, sur un support jetable créé puis supprimé pour
l'occasion) : 5 images vides ajoutées à la suite produisent bien 3
pages, chacune avec le cadre fixe "Couvrir" actif par défaut.
Audit final : chaque ligne du tableau d'audit initial pour
l'élément Image est couverte — implémentée (dimensionnement/ratio/
object-fit, bordure+ombre via box_style.py, filtre, lien/lightbox/
lazy-load/légende, position du bloc dont l'alignement, upload de
fichier) ou explicitement différée ci-dessus avec sa raison
(object-position, mix-blend-mode, clip-path, overlay au survol,
arrondi par coin, responsive par taille d'écran — mêmes exclusions que
Titre/Paragraphe, valeur douteuse pour du contenu de formation). Rien
d'oublié constaté à cette relecture. Validé par l'utilisateur, prêt à
committer.
3. Bouton — ✅ audité et validé (commité)
Implémenté : gras/italique, majuscules/minuscules/capitales, police
(liste déroulante web-safe, réutilise FORGE_DOC_FONT_FAMILY_OPTIONS
déjà créé pour Titre/Paragraphe), taille de police, espacement des
lettres, couleur du texte (sélecteur natif), icône SVG optionnelle
(position avant/après le texte, taille réglable — le bouton dépasse ici
l'étiquette, qui n'a toujours qu'une icône fixe sans position ni taille
réglables), tous les attributs de boîte partagés (padding, margin,
couleur de fond, arrondi, largeur/hauteur, ombre portée, opacité,
position du bloc, bordure par côté). États interactifs survol/actif :
effet visuel UNIVERSEL (assombrissement léger au survol, léger
tassement au clic), automatique pour tous les boutons sans réglage à
faire — jamais une couleur de survol personnalisable par bouton
(mécanisme CSS fragile pour une valeur ajoutée faible en contenu de
formation). Curseur (cursor:pointer) et transition au survol déjà en
place.
Mécanisme partagé réutilisé tel quel : document_engine/rendering/ box_style.py (comme Titre/Paragraphe/Image) + sanitize_svg_markup
(comme l'Image et l'Étiquette pour l'icône). Extraction d'une constante
JS partagée FORGE_DOC_TEXT_TRANSFORM_OPTIONS (utilisée par Titre/
Paragraphe ET Bouton, plus de duplication de ce tableau d'options).
Volontairement laissé de côté (même rationale que Titre/Paragraphe/ Image) : dégradé/image de fond, arrondi par coin (un seul rayon pour les 4 coins), réglages responsive par taille d'écran. État "désactivé" délibérément absent : un bouton de contenu de formation n'est pas un vrai contrôle de formulaire avec un état programmatique désactivé — aucun équivalent clair sans inventer un concept artificiel; à ajouter si un besoin précis se présente.
Audit final : chaque ligne du tableau d'audit initial pour
l'élément Bouton est couverte — typo (police/taille/gras/italique/
transformation/espacement), boîte (padding/margin/largeur/hauteur/
position du bloc), bordure/ombre (style/couleur/épaisseur/par côté +
le nouveau réglage "les 4 côtés à la fois"/ombre portée), fond
(couleur), icône, effets (transition/curseur) — chacune implémentée ou
explicitement différée ci-dessus avec sa raison (dégradé/image de fond,
arrondi par coin, responsive, état désactivé). 9 tests dédiés passent
(224 au total dans tests/document/), ruff/mypy --strict/bandit/
vulture/import-linter/eslint/stylelint tous clean. Rien d'oublié
constaté à cette relecture. Validé par l'utilisateur, prêt à committer.
Ajout transversal pendant le test (retour utilisateur : "pour les
bordures, il faudrait une option pour modifier les 4 bordures en même
temps") : une rangée "Bordure — les 4 côtés à la fois" a été ajoutée
au-dessus du réglage par côté existant, dans le module PARTAGÉ
forgeDocRenderBoxFieldsHtml/forgeDocBindBoxFields (static/document/
js/document-editor.js) — un changement de style/épaisseur/couleur y
applique la MÊME valeur aux 4 côtés d'un coup (en plus, jamais à la
place, du réglage par côté qui reste utilisable après pour affiner).
Repart de l'état actuel si les 4 côtés portent déjà la même valeur,
sinon d'un état neutre. Bénéficie automatiquement à TOUS les éléments
qui utilisent déjà ce module (Titre/Paragraphe/Image/Bouton), aucune
modification nécessaire ailleurs.
4. Liste à puces / numérotée — ✅ audité et validé (commité)
Portée actée avec l'utilisateur avant implémentation (question
posée explicitement, deux catégories du tableau impliquaient de
transformer items — une simple liste de chaînes — en objets
structurés) : style sur la LISTE ENTIÈRE uniquement, jamais par élément
individuel ni de sous-listes imbriquées (chantier bien plus lourd,
différé à une demande séparée si besoin). Seule exception actée : un
padding UNIFORME appliqué à chaque élément (item_padding), pas encore
réglable individuellement.
Implémenté : gras/italique/souligné, police (liste déroulante web-safe,
FORGE_DOC_FONT_FAMILY_OPTIONS réutilisé), taille de police, hauteur de
ligne, couleur du texte, style de puce (list_style_type, options
propres à chaque kind — disque/cercle/carré/aucune pour puces,
1-2-3/01-02-03/i-ii-iii/I-II-III/a-b-c/A-B-C/aucune pour numérotée),
position de la puce (intérieure/extérieure), couleur et taille de puce
indépendantes du texte, puce personnalisée (icône SVG, liste à puces
uniquement — encodée en URI de données pour list-style-image),
espacement intérieur par élément (item_padding, "6px" par défaut,
retour utilisateur explicite : "il faut un padding de base par élément
de liste car y en a pas aujourd'hui" — n'existait pas du tout avant),
espacement entre éléments réglable (item_spacing), tous les attributs
de boîte partagés sur la liste entière (padding/margin/fond/bordure/
ombre/opacité/position du bloc).
Mécanisme technique notable : marker_color/marker_size/
item_padding/item_spacing ne peuvent pas passer par un style inline
classique sur le <ul>/<ol> (impossible de cibler le ::marker ou le
padding des <li> enfants depuis le style de leur parent) — résolu via
des propriétés personnalisées CSS (--doc-marker-color etc.), posées
en inline sur le conteneur et consommées par une règle CSS dédiée
(.docList li/.docList li::marker) qui en hérite. Même technique déjà
utilisée pour la position du bloc des éléments enveloppés (Image).
Volontairement laissé de côté (portée actée ci-dessus) : style/bordure/
fond par élément individuel, sous-listes imbriquées — nécessiteraient
de transformer items (liste de chaînes) en objets structurés, refonte
du panneau et du stockage. Comme les autres éléments : responsive par
taille d'écran.
Existant retroactivement inchangé : une liste déjà créée avant ce
commit garde ses anciens attributs (juste items) — item_padding
n'apparaît en style inline QUE pour les nouvelles listes ; ouvrir le
panneau Propriétés d'une ancienne liste et toucher un réglage la fait
bénéficier des nouveaux défauts au passage.
Bug réel corrigé pendant le test (retour utilisateur : "si j'enlève
les puces ou que les puces se mettent à l'intérieur, il reste un
espace devant la liste, cet espace doit être supprimé") : le
padding-left:1.4em par défaut (réservé pour une puce EXTÉRIEURE)
n'a plus lieu d'être dès que list_style_type="none" ou
list_style_position="inside" — _render_list ajoute alors
automatiquement padding-left:0;, prioritaire sur le CSS mais toujours
cédant la place à un padding uniforme réglé explicitement par
ailleurs (attributs de boîte partagés).
Deux ajouts transversaux pendant le test, bénéficiant à plusieurs éléments à la fois :
- Sections "Contenu"/"Conteneur" dans le panneau Propriétés (retour
utilisateur : "il faut distinguer par des sections la propriété qui
touche au conteneur de celles qui touchent à l'élément qu'il
contient sinon c'est pas compréhensible") : un sous-titre visuel
(bordure du dessus) sépare maintenant, dans CHAQUE panneau qui
utilise
box_style.py(Titre/Paragraphe, Image, Bouton, Liste), les champs propres au CONTENU (texte, icône, puces...) de ceux qui touchent au CONTENEUR (padding/margin/fond/bordure/ombre/opacité/ position du bloc/alignement du contenu — voir point 2). Le sous-titre "Conteneur" est posé UNE SEULE FOIS, à l'intérieur deforgeDocRenderBoxFieldsHtml(le module déjà partagé), jamais dupliqué à chaque appelant. - Alignement vertical du CONTENU dans son bloc (retour utilisateur :
"je peux augmenter la hauteur d'un conteneur mais pas l'alignement
vertical à l'intérieur, pour les listes et il faut aussi vérifier
pour les autres") : nouvel attribut partagé
content_align("top"par défaut,"center"/"bottom") posé dansBOX_DEFAULTS, rendu viarender_content_align(a)— volontairement PAS fusionné dansrender_box_style(l'alignement du contenu dépend de l'axe interne du conteneur :justify-contentconvient à un conteneur en COLONNE — Titre/Paragraphe/Liste, rendusdisplay:flex; flex-direction:column;pour l'occasion, et la figure d'une image légendée, déjà flex-colonne — mais serait FAUX pour le Bouton, une RANGÉE icône+texte qui gère déjà son axe vertical viaalign-items, déjà correct sans réglage). Contrôle exclu explicitement du panneau Bouton (includeContentAlign: false) pour ne jamais afficher un réglage sans effet.
Bug réel corrigé, puis CORRIGÉ UNE SECONDE FOIS après un retour de régression (capture à l'appui, thème "Sécurité incendie") :
- Premier symptôme ("dans les liste numéroté [...] la position des
élément à l'intérieur de base est verticale au lieu d'être
horizontale") : ce thème remplace la puce native d'une liste par un
badge (
::before, voirstatic/document/themes/ securite-incendie.css), pensé pour s'afficher À CÔTÉ du texte. Corrigé une première fois en posantdisplay:flexsur.docList li(static/document/document-editor.css). - Régression introduite par ce premier correctif ("quand j'enlève le
thème [...] on voit plus les puces ou les numéros") :
display:flexposé DIRECTEMENT sur le<li>remplace entièrement sondisplay:list-itemnatif — ça supprime le marqueur natif (puce/ numéro) pour TOUTE liste, avec ou sans thème (l'affirmation inverse dans le premier correctif était FAUSSE, corrigée après une vérification en conditions réelles, navigateur automatisé). Corrigé en ciblant le::beforeLUI-MÊME (display:inline-block; vertical-align:middle; margin-right:10px;), jamais son parent : le<li>garde sondisplay:list-itemnatif (donc son marqueur), et le badge s'aligne quand même à côté du texte qui le suit dans le flux normal. - Redondance restante : une fois le marqueur natif restauré, les
listes du thème affichaient À LA FOIS leur badge personnalisé ET le
marqueur natif (disc/decimal) en double — le thème ne les avait
jamais explicitement désactivés (aucun
list-style:nonedanssecurite-incendie.css, il comptait implicitement sur le marqueur natif pour disparaître tout seul). Corrigé en ajoutant ce reset — avec le sélecteur d'élément (ul.docList[...]/ol.docList[...]), jamais seulement les classes/attributs :static/style.css(site large) porte une règle.content ol:not([type]) { list-style-type: decimal; }d'une spécificité légèrement supérieure qui l'emportait sinon silencieusement. - Dernière régression du même correctif : le badge numéroté du thème
posait lui-même
display:flex(pour centrer son chiffre) — une valeur qui BLOCKIFIE le::before(le repousse hors du flux en ligne, au-dessus du texte), défaisant le point 1 pour ce cas précis. Corrigé endisplay:inline-flex(garde le badge en ligne tout en centrant quand même le chiffre à l'intérieur).
Chaque étape vérifiée par un navigateur automatisé (Playwright) contre
le serveur local, sur un support jetable créé puis supprimé pour
l'occasion — thème appliqué et retiré successivement, capture d'écran
et styles calculés (getComputedStyle) inspectés à chaque fois, pas
seulement supposés corrects.
Audit final : chaque ligne du tableau d'audit initial pour l'élément
Liste est couverte —
Typo (police/taille/gras/italique/souligné/couleur/interligne) ✅,
Puces/numéros (style de puce validé par kind, image de puce
personnalisée pour les puces, position intérieure/extérieure, couleur/
taille de puce indépendantes du texte via propriétés personnalisées
CSS) ✅,
Boîte (padding/margin sur la liste entière via box_style.py,
padding UNIFORME par élément — item_padding, avec un vrai défaut non
vide comme demandé —, espacement entre éléments réglable —
item_spacing —, indentation couverte par le padding partagé) ✅,
Bordure/fond sur la liste entière ✅.
Volontairement différés, portée actée explicitement AVANT
implémentation avec l'utilisateur (question posée, réponse : liste
entière seulement) : bordure/fond/padding/margin par élément
individuel (au-delà du padding uniforme) et listes imbriquées —
transformeraient items (liste de chaînes) en objets structurés, un
chantier bien plus lourd que le reste du tableau, à traiter séparément
si demandé. Comme les autres éléments : responsive par taille d'écran.
245 tests passent, ruff/mypy --strict/bandit/vulture/import-linter/
eslint/stylelint tous clean. Rien d'oublié constaté à cette relecture.
Validé par l'utilisateur (y compris les 4 bugs de thème trouvés et
corrigés pendant la validation), prêt à committer.
5. Étiquette (badge) — à compléter
Déjà fait (session précédente) : icône SVG, largeur, arrondi uniforme,
gras, majuscules. Restant selon le tableau ci-dessus : italique,
souligné, barré, police, taille de police, couleur du texte, espacement
des lettres, hauteur, padding, margin, bordure par côté, ombre portée,
position de l'icône. Pourra réutiliser directement box_style.py.
6. Carte — à faire
7. Rangée (row) — à faire
8. Mini-jeux — à faire
Décisions transversales actées pendant ce chantier
- Un seul rayon d'arrondi pour les 4 coins (pas par coin) — cohérent avec l'étiquette déjà en place, à revoir si demandé explicitement.
- Padding/margin : une valeur UNIFORME sur les 4 côtés (curseur unique), pas par côté — la bordure, elle, reste par côté (demande explicite).
- Ombres (texte et bloc) : 3 préréglages (Aucune/Légère/Marquée) plutôt que des curseurs séparés offset/flou/couleur — reste simple à utiliser, ajustable si besoin de plus de finesse plus tard.
- Polices : liste fermée de polices web-safe (aucun chargement dynamique de Google Fonts depuis l'éditeur) — évite d'introduire un mécanisme de chargement de police, hors périmètre de cet audit.
Fonctionnalité hors tableau : glisser-déposer un élément vers une autre page
Retour utilisateur du 26/09/2026 : "j'aimerais pouvoir glisser déposer
un élément d'une page dans une autre page" — sans rapport avec l'audit
de mise en forme élément par élément, mais traité dans la foulée.
Le mécanisme bas niveau (document_engine.move_document_element_to_page
- la route
/elements/<id>/move-to-page) existait déjà, utilisé uniquement par la pagination automatique (voirforgeDocCheckPageOverflow) — déjà bien testé côté serveur (test_document_elements.py/test_document_routes.py).
Ajout : glisser un élément du canevas jusqu'à une rangée du panneau
Pages (onglet "Pages" du panneau gauche) le déplace vers cette page —
même charge utile de glisser ("text/forge-doc-move") que le
réordonnancement au sein d'une page, aucune modification de la source
de glisser côté canevas. Nouvelle fonction forgeDocMoveElementToPage
(commande Annuler/Rétablir complète, comme le reste des mutations).
Surlignage visuel de la rangée survolée (.docPageRow--dropTarget),
distinct de .is-active/.is-dragging. Pas de test automatisé
possible côté client (aucune suite de tests n'existe pour
document-editor.js), vérification manuelle uniquement — le mécanisme
serveur sous-jacent, lui, reste couvert par les tests existants.
Fonctionnalité hors tableau : retirer le thème appliqué
Retour utilisateur du 26/09/2026 : après avoir choisi un thème puis vidé toutes les pages, le thème restait appliqué — question légitime ("c'est voulu ?"), réponse : OUI pour la séparation contenu/thème (déjà le cas), mais il manquait un moyen de retirer un thème une fois choisi. Ajout demandé : "dans la modale de choix des modèles, ajoute-en un qui s'appelle Aucun modèle, si l'utilisateur le choisit ça enlève tout modèle de style choisi pour revenir à un document de base".
- Nouvelle fonction bas niveau
db.remove_document_theme(slug)(db/supports/remove_document_theme.py) — supprime la LIGNE_metaplutôt que d'y stocker une chaîne vide, pour queget_document_themecontinue de renvoyerNone(son contrat documenté : "aucun thème n'a jamais été appliqué"), jamais une chaîne vide qui le violerait silencieusement pour tout appelant qui compare àNone(dont le test déjà existanttest_new_support_has_no_theme_by_default). - Route
/document/<slug>/theme/apply:theme_idvide retire désormais le thème et s'arrête là —moden'a alors aucun sens (aucun contenu de démonstration pour "aucun modèle") et est ignoré, jamais validé ni utilisé dans ce cas. - Modale "Utiliser un modèle" : nouvelle carte "Aucun modèle"
toujours en tête de liste (même catalogue vide), id sentinelle
""— distincte denull(qui reste réservé à "rien n'a encore été cliqué dans la modale",forgeDocSelectedTemplateIdà l'ouverture). La sélectionner remplace l'aperçu (rien à prévisualiser, "aucun modèle" n'a pas de contenu de démonstration) par un message explicite et un unique bouton "Retirer le modèle" (jamais les deux boutons "contenu actuel/du modèle", qui supposent un vrai thème choisi) — confirmation native avant l'action (changement visuel notable, même si le contenu n'est jamais touché). - Testé :
db/supports/remove_document_theme.py(2 tests bas niveau,tests/document/test_support_lifecycle.py) + la route (2 tests,tests/document/test_document_routes.py— retire vraiment le thème, laisse le contenu intact, ignoremode). Le déclenchement côté client (clic sur la carte/le bouton) reste manuel faute de suite de tests JS, comme le reste de l'éditeur.