# 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) 1. Un élément du tableau à la fois, dans l'ordre. 2. J'implémente tout ce qui manque pour cet élément. 3. Je préviens l'utilisateur, qui teste et valide/invalide. 4. Dernier audit sur l'élément pour vérifier que rien n'est oublié. 5. 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 `` 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-index`
**Effets** : 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-position`
**Bordure/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 survol
**Comportement** : lien cliquable, ouverture en plein écran/lightbox au clic, chargement différé (`loading=lazy`), légende (caption) affichée sous l'image
**Position** : alignement horizontal dans son conteneur, `margin`, `padding` autour
**Responsive** : 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 texte
**Boî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-shadow`
**Position 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-index`
**Responsive** : 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 `
/
`), 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'``/`
` INTERNE, jamais à son enveloppe (`
`/``/`
`) 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 `
    `/`
      ` (impossible de cibler le `::marker` ou le padding des `
    1. ` 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 :** 1. **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 de `forgeDocRenderBoxFieldsHtml` (le module déjà partagé), jamais dupliqué à chaque appelant. 2. **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é dans `BOX_DEFAULTS`, rendu via `render_content_align(a)` — volontairement PAS fusionné dans `render_box_style` (l'alignement du contenu dépend de l'axe interne du conteneur : `justify-content` convient à un conteneur en COLONNE — Titre/Paragraphe/Liste, rendus `display: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 via `align-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")** : 1. *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`, voir `static/document/themes/ securite-incendie.css`), pensé pour s'afficher À CÔTÉ du texte. Corrigé une première fois en posant `display:flex` sur `.docList li` (`static/document/document-editor.css`). 2. *Régression introduite par ce premier correctif* ("quand j'enlève le thème [...] on voit plus les puces ou les numéros") : `display:flex` posé DIRECTEMENT sur le `
    2. ` remplace entièrement son `display:list-item` natif — ç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 `::before` LUI-MÊME** (`display:inline-block; vertical-align:middle; margin-right:10px;`), jamais son parent : le `
    3. ` garde son `display:list-item` natif (donc son marqueur), et le badge s'aligne quand même à côté du texte qui le suit dans le flux normal. 3. *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:none` dans `securite-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. 4. *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é en `display: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//move-to-page`) existait déjà, utilisé uniquement par la pagination automatique (voir `forgeDocCheckPageOverflow`) — 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 `_meta` plutôt que d'y stocker une chaîne vide, pour que `get_document_theme` continue de renvoyer `None` (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à existant `test_new_support_has_no_theme_by_default`). - **Route** `/document//theme/apply` : `theme_id` vide retire désormais le thème et s'arrête là — `mode` n'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 de `null` (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, ignore `mode`). 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.