Audit complet de mise en forme — Liste à puces/numérotée (4e élément)

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>
This commit is contained in:
william
2026-09-26 15:13:14 +02:00
co-authored by Claude Sonnet 5
parent 8f879f6321
commit 4db1269348
17 changed files with 1080 additions and 34 deletions
+61 -1
View File
@@ -263,6 +263,16 @@ body.objectEditBody > main.content {
margin-bottom: 10px;
}
/* Sépare visuellement, DANS un même panneau Propriétés, les champs qui
touchent au CONTENU (propre à chaque kind) de ceux qui touchent au
CONTENEUR (position/boîte, partagés — voir forgeDocRenderBoxFieldsHtml)
— retour utilisateur du 26/09/2026 : "sinon c'est pas compréhensible". */
.docPropsSectionLabel {
margin-top: 20px;
padding-top: 14px;
border-top: 1px solid var(--doc-border);
}
.docLibItem {
border: 1px solid var(--doc-border);
border-radius: 10px;
@@ -587,6 +597,16 @@ body.objectEditBody > main.content {
mots normaux au milieu sans raison) ne coupe QUE si le mot ne tient
vraiment pas sur une ligne. */
overflow-wrap: break-word;
/* flex-colonne (retour utilisateur du 26/09/2026 : "je peux augmenter
la hauteur d'un conteneur mais pas l'alignement vertical à
l'intérieur") : sans effet visuel tant que la hauteur du bloc
correspond à son contenu (comportement historique inchangé) —
n'entre en jeu que si une hauteur fixe/minimale (voir box_style.py)
dépasse la hauteur naturelle du texte, via justify-content
(render_content_align, appliqué uniquement si réglé). */
display: flex;
flex-direction: column;
}
.docImage.docImagePlaceholder {
@@ -687,8 +707,48 @@ img.docImage {
color: var(--forge-text);
}
/* item_padding/item_spacing/marker_color/marker_size (retour utilisateur
du 26/09/2026, audit — colonne "Liste à puces/numérotée") : une
valeur inline sur le <ul>/<ol> ne peut pas cibler directement un <li>
enfant ni son ::marker — passe par des PROPRIÉTÉS PERSONNALISÉES CSS,
posées en style inline sur le conteneur (voir _render_list,
document_engine/rendering/render_document_element.py) et héritées
jusqu'ici (les propriétés personnalisées traversent aussi bien les
descendants que leurs pseudo-éléments). Valeur par défaut = comportement
historique inchangé (4px entre éléments, pas de padding) tant qu'aucune
valeur n'est réglée. */
.docList li {
padding: var(--doc-item-padding, 0);
}
/* Bug réel corrigé une première fois le 26/09/2026 (capture à l'appui,
thème "Sécurité incendie") en posant display:flex ICI sur <li> pour
que le badge en ::before d'un thème (voir static/document/themes/
securite-incendie.css) s'affiche À CÔTÉ du texte plutôt qu'AU-DESSUS.
**Corrigé une seconde fois** (retour utilisateur : "quand j'enlève le
thème [...] on voit plus les puces ou les numéros") : display:flex
sur le <li> lui-même REMPLACE entièrement son display:list-item —
ça supprime le marqueur natif (puce/numéro) pour TOUTE liste, avec ou
sans thème, pas seulement pour celles qui ont un ::before (contraire
à ce qu'affirmait le commentaire précédent, qui était FAUX — vérifié
par un test en conditions réelles). La bonne cible pour la mise en
ligne du badge est le ::before LUI-MÊME (inline-block, jamais son
parent) : le <li> garde son display natif, donc son marqueur, et le
badge d'un thème s'aligne quand même à côté du texte qui le suit
immédiatement dans le flux normal. */
.docList li::before {
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
.docList li + li {
margin-top: 4px;
margin-top: var(--doc-item-spacing, 4px);
}
.docList li::marker {
color: var(--doc-marker-color, inherit);
font-size: var(--doc-marker-size, inherit);
}
.docMinigame {
+197 -13
View File
@@ -1966,6 +1966,43 @@ const FORGE_DOC_FONT_FAMILY_OPTIONS = [
['"Courier New", monospace', 'Courier New (machine à écrire)'],
];
// Retour utilisateur du 26/09/2026 : "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
// simple sous-titre visuel (bordure du dessus), posé entre les champs
// de CONTENU (propres à chaque kind) et les champs de CONTENEUR
// (position/boîte, partagés via forgeDocRenderBoxFieldsHtml), dans
// CHAQUE panneau de propriétés.
function forgeDocPropsSectionHtml(label) {
return `<div class="docSectionLabel docPropsSectionLabel">${label}</div>`;
}
const FORGE_DOC_CONTENT_ALIGN_OPTIONS = [['top', 'Haut'], ['center', 'Centré'], ['bottom', 'Bas']];
function forgeDocRenderContentAlignHtml(a, idPrefix) {
// Retour utilisateur du 26/09/2026 : "je peux augmenter la hauteur
// d'un conteneur mais pas l'alignement vertical à l'intérieur" — sans
// effet tant que la hauteur du bloc correspond à son contenu, voir
// render_content_align (document_engine/rendering/box_style.py).
// Jamais posé pour le Bouton (rangée icône+texte, l'axe vertical y
// est déjà géré par align-items, voir document-editor.css).
const align = a.content_align || 'top';
return `
<div class="docField">
<span class="docFieldLabel">Alignement vertical du contenu</span>
<div class="docSegmented" id="${idPrefix}ContentAlignSeg">
${FORGE_DOC_CONTENT_ALIGN_OPTIONS.map(([v, l]) => `<button type="button" class="docSegBtn ${align === v ? 'is-active' : ''}" data-content-align="${v}">${l}</button>`).join('')}
</div>
</div>
`;
}
function forgeDocBindContentAlign(panel, idPrefix, patch) {
panel.querySelectorAll(`#${idPrefix}ContentAlignSeg .docSegBtn`).forEach((btn) => {
btn.addEventListener('click', () => patch({ content_align: btn.dataset.contentAlign }));
});
}
/* ---------------------------------------------------------------------
* Contrôles génériques réutilisables — retour utilisateur du 26/09/2026 :
* "jamais l'utilisateur ne doit rentrer une valeur brute à la main" —
@@ -2019,11 +2056,20 @@ function forgeDocToHexColor(value, fallback) {
return /^#[0-9a-f]{6}$/i.test(value || '') ? value : fallback;
}
function forgeDocRenderBoxFieldsHtml(a, idPrefix) {
function forgeDocRenderBoxFieldsHtml(a, idPrefix, opts = {}) {
// Retour utilisateur du 26/09/2026 : "il faut distinguer par des
// sections la propriété qui touche au conteneur de celles qui
// touchent à l'élément qu'il contient" — ce bloc EST la section
// "Conteneur" pour tous les panneaux qui l'appellent (le contenu
// propre à chaque kind vient avant, dans son propre appelant).
// includeContentAlign=false pour le Bouton uniquement (rangée
// icône+texte, l'axe vertical y est déjà géré par align-items).
const includeContentAlign = opts.includeContentAlign !== false;
const border = a.border || {};
const alignSelf = a.align_self || 'stretch';
const opacityPercent = a.opacity !== '' && a.opacity !== undefined ? Math.round(Number.parseFloat(a.opacity) * 100) : '';
return `
${forgeDocPropsSectionHtml('Conteneur')}
${forgeDocRenderToggleSliderHtml(`${idPrefix}Padding`, 'Espacement intérieur (padding)', a.padding, {
min: 0, max: 80, step: 2, unit: 'px', fallback: 16,
})}
@@ -2073,6 +2119,7 @@ function forgeDocRenderBoxFieldsHtml(a, idPrefix) {
${FORGE_DOC_ALIGN_SELF_OPTIONS.map(([v, l]) => `<button type="button" class="docSegBtn ${alignSelf === v ? 'is-active' : ''}" data-align-self="${v}">${l}</button>`).join('')}
</div>
</div>
${includeContentAlign ? forgeDocRenderContentAlignHtml(a, idPrefix) : ''}
<div class="docField">
<span class="docFieldLabel">Bordure — les 4 côtés à la fois</span>
<div class="docBorderSideRow" id="${idPrefix}BorderAllRow">
@@ -2121,7 +2168,8 @@ function forgeDocRenderBoxFieldsHtml(a, idPrefix) {
`;
}
function forgeDocBindBoxFields(panel, el, idPrefix, patch) {
function forgeDocBindBoxFields(panel, el, idPrefix, patch, opts = {}) {
if (opts.includeContentAlign !== false) forgeDocBindContentAlign(panel, idPrefix, patch);
forgeDocBindToggleSlider(`${idPrefix}Padding`, { unit: 'px' }, (v) => patch({ padding: v }));
forgeDocBindToggleSlider(`${idPrefix}Margin`, { unit: 'px' }, (v) => patch({ margin: v }));
forgeDocBindToggleSlider(`${idPrefix}Radius`, { unit: 'px' }, (v) => patch({ border_radius: v }));
@@ -2191,6 +2239,7 @@ function forgeDocRenderTextProps(panel, el) {
const a = el.attributes;
panel.innerHTML = `
${forgeDocPropsHeader('texte')}
${forgeDocPropsSectionHtml('Contenu')}
<div class="docField">
<span class="docFieldLabel">Style</span>
<div class="docSegmented" id="docStyleSeg">
@@ -2295,6 +2344,7 @@ function forgeDocRenderImageProps(panel, el) {
const clickBehavior = a.click_behavior || '';
panel.innerHTML = `
${forgeDocPropsHeader('image')}
${forgeDocPropsSectionHtml('Contenu')}
<div class="docField">
<span class="docFieldLabel">Fichier image</span>
<div id="docImageUploadStatus">${a.src ? 'Image actuelle en place' : 'Aucun fichier choisi'}</div>
@@ -2395,6 +2445,7 @@ function forgeDocRenderButtonProps(panel, el) {
: 'Aucun fichier joint';
panel.innerHTML = `
${forgeDocPropsHeader('bouton')}
${forgeDocPropsSectionHtml('Contenu')}
<div class="docField"><label for="docButtonLabel">Texte du bouton</label><input type="text" id="docButtonLabel" value="${forgeDocEscapeHtml(a.label || '')}"></div>
<div class="docField"><label for="docButtonTarget">Cible (URL ou ancre)</label><input type="text" id="docButtonTarget" value="${forgeDocEscapeHtml(a.target || '')}"></div>
<div class="docField">
@@ -2441,7 +2492,7 @@ function forgeDocRenderButtonProps(panel, el) {
${forgeDocRenderToggleSliderHtml('docButtonLetterSpacing', 'Espacement des lettres', a.letter_spacing, {
min: -2, max: 10, step: 0.5, unit: 'px', fallback: 0,
})}
${forgeDocRenderBoxFieldsHtml(a, 'docButton')}
${forgeDocRenderBoxFieldsHtml(a, 'docButton', { includeContentAlign: false })}
${forgeDocDeleteButtonHtml()}
`;
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
@@ -2478,7 +2529,7 @@ function forgeDocRenderButtonProps(panel, el) {
document.getElementById('docButtonFontFamilyInput').addEventListener('change', (e) => patch({ font_family: e.target.value }));
forgeDocBindToggleSlider('docButtonFontSize', { unit: 'px' }, (v) => patch({ font_size: v }));
forgeDocBindToggleSlider('docButtonLetterSpacing', { unit: 'px' }, (v) => patch({ letter_spacing: v }));
forgeDocBindBoxFields(panel, el, 'docButton', patch);
forgeDocBindBoxFields(panel, el, 'docButton', patch, { includeContentAlign: false });
}
function forgeDocRenderBadgeProps(panel, el) {
@@ -2531,14 +2582,30 @@ function forgeDocRenderListItemHtml(item, index) {
`;
}
const FORGE_DOC_LIST_STYLE_TYPE_OPTIONS = {
liste_puces: [['', 'Par défaut'], ['disc', 'Disque'], ['circle', 'Cercle'], ['square', 'Carré'], ['none', 'Aucune']],
liste_numerotee: [
['', 'Par défaut'], ['decimal', '1, 2, 3'], ['decimal-leading-zero', '01, 02, 03'],
['lower-roman', 'i, ii, iii'], ['upper-roman', 'I, II, III'],
['lower-alpha', 'a, b, c'], ['upper-alpha', 'A, B, C'], ['none', 'Aucune'],
],
};
const FORGE_DOC_LIST_STYLE_POSITION_OPTIONS = [['outside', 'Extérieure'], ['inside', 'Intérieure']];
function forgeDocRenderListProps(panel, el) {
// Même patron répéteur que l'Association/Memory (ajouter/supprimer/
// modifier une ligne, panneau entièrement rechargé après chaque
// modification via patch()) — ici une simple liste de chaînes, pas de
// structure à 2 champs comme une paire.
// structure à 2 champs comme une paire. Mise en forme : sur la LISTE
// ENTIÈRE uniquement (retour utilisateur du 26/09/2026 : pas de style
// par élément individuel ni de sous-listes, portée actée explicitement
// — voir document_engine/labels/element_kind_labels.py), à l'exception
// du padding par élément (item_padding, UNIFORME, voir plus bas).
const a = el.attributes;
const items = a.items || [];
const kindLabel = el.kind === 'liste_numerotee' ? 'liste numérotée' : 'liste à puces';
const isPuces = el.kind !== 'liste_numerotee';
const kindLabel = isPuces ? 'liste à puces' : 'liste numérotée';
const styleTypeOptions = FORGE_DOC_LIST_STYLE_TYPE_OPTIONS[el.kind] || FORGE_DOC_LIST_STYLE_TYPE_OPTIONS.liste_puces;
function patch(partial) {
forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }).then(() => {
@@ -2548,9 +2615,63 @@ function forgeDocRenderListProps(panel, el) {
panel.innerHTML = `
${forgeDocPropsHeader(kindLabel)}
${forgeDocPropsSectionHtml('Contenu')}
<div class="docSectionLabel docQuizQuestionsLabel">Éléments de la liste</div>
<div id="docListItemsList">${items.length ? items.map(forgeDocRenderListItemHtml).join('') : '<div class="docEmptyState">Aucun élément — ajoute le premier ci-dessous.</div>'}</div>
<button type="button" class="docBtnSecondary docQuizAddBtn" id="docListAddItem">+ Ajouter un élément</button>
<div class="docField">
<span class="docFieldLabel">Mise en forme du texte</span>
<div class="docToolbarRow">
<button type="button" class="docFmtBtn ${a.bold ? 'is-active' : ''}" id="docListBoldBtn" title="Gras">B</button>
<button type="button" class="docFmtBtn docFmtBtn--italic ${a.italic ? 'is-active' : ''}" id="docListItalicBtn" title="Italique">I</button>
<button type="button" class="docFmtBtn docFmtBtn--underline ${a.underline ? 'is-active' : ''}" id="docListUnderlineBtn" title="Souligné">U</button>
</div>
</div>
<div class="docField">
<label for="docListColorInput">Couleur du texte</label>
<input type="color" id="docListColorInput" value="${forgeDocToHexColor(a.text_color, '#e8ecf4')}">
</div>
<div class="docField">
<label for="docListFontFamilyInput">Police de caractère</label>
<select id="docListFontFamilyInput">
${FORGE_DOC_FONT_FAMILY_OPTIONS.map(([v, l]) => `<option value="${forgeDocEscapeHtml(v)}" ${(a.font_family || '') === v ? 'selected' : ''}>${l}</option>`).join('')}
</select>
</div>
${forgeDocRenderToggleSliderHtml('docListFontSize', 'Taille de police', a.font_size, {
min: 10, max: 32, step: 1, unit: 'px', fallback: 15,
})}
${forgeDocRenderToggleSliderHtml('docListLineHeight', 'Hauteur de ligne', a.line_height, {
min: 1, max: 2.5, step: 0.1, unit: '', fallback: 1.6,
})}
<div class="docField">
<span class="docFieldLabel">Style de puce</span>
<div class="docSegmented" id="docListStyleTypeSeg">
${styleTypeOptions.map(([v, l]) => `<button type="button" class="docSegBtn ${(a.list_style_type || '') === v ? 'is-active' : ''}" data-style-type="${v}">${l}</button>`).join('')}
</div>
</div>
<div class="docField">
<span class="docFieldLabel">Position de la puce</span>
<div class="docSegmented" id="docListStylePositionSeg">
${FORGE_DOC_LIST_STYLE_POSITION_OPTIONS.map(([v, l]) => `<button type="button" class="docSegBtn ${(a.list_style_position || 'outside') === v ? 'is-active' : ''}" data-position="${v}">${l}</button>`).join('')}
</div>
</div>
<div class="docField">
<label for="docListMarkerColorInput">Couleur de la puce</label>
<input type="color" id="docListMarkerColorInput" value="${forgeDocToHexColor(a.marker_color, '#e8ecf4')}">
</div>
${forgeDocRenderToggleSliderHtml('docListMarkerSize', 'Taille de la puce', a.marker_size, {
min: 10, max: 40, step: 1, unit: 'px', fallback: 15,
})}
${isPuces ? `
<div class="docField"><label for="docListSvg">Puce personnalisée (icône SVG, optionnel)</label><textarea id="docListSvg" rows="3">${forgeDocEscapeHtml(a.svg_markup || '')}</textarea></div>
` : ''}
${forgeDocRenderBoxFieldsHtml(a, 'docList')}
${forgeDocRenderToggleSliderHtml('docListItemPadding', 'Espacement intérieur par élément', a.item_padding, {
min: 0, max: 40, step: 2, unit: 'px', fallback: 6,
})}
${forgeDocRenderToggleSliderHtml('docListItemSpacing', 'Espacement entre les éléments', a.item_spacing, {
min: 0, max: 40, step: 2, unit: 'px', fallback: 4,
})}
${forgeDocDeleteButtonHtml()}
`;
@@ -2571,6 +2692,28 @@ function forgeDocRenderListProps(panel, el) {
patch({ items: items.map((it, i) => (i === index ? e.target.value : it)) });
});
});
document.getElementById('docListBoldBtn').addEventListener('click', () => patch({ bold: !a.bold }));
document.getElementById('docListItalicBtn').addEventListener('click', () => patch({ italic: !a.italic }));
document.getElementById('docListUnderlineBtn').addEventListener('click', () => patch({ underline: !a.underline }));
document.getElementById('docListColorInput').addEventListener('change', (e) => patch({ text_color: e.target.value }));
document.getElementById('docListFontFamilyInput').addEventListener('change', (e) => patch({ font_family: e.target.value }));
forgeDocBindToggleSlider('docListFontSize', { unit: 'px' }, (v) => patch({ font_size: v }));
forgeDocBindToggleSlider('docListLineHeight', { unit: '' }, (v) => patch({ line_height: v }));
panel.querySelectorAll('#docListStyleTypeSeg .docSegBtn').forEach((btn) => {
btn.addEventListener('click', () => patch({ list_style_type: btn.dataset.styleType }));
});
panel.querySelectorAll('#docListStylePositionSeg .docSegBtn').forEach((btn) => {
btn.addEventListener('click', () => patch({ list_style_position: btn.dataset.position }));
});
document.getElementById('docListMarkerColorInput').addEventListener('change', (e) => patch({ marker_color: e.target.value }));
forgeDocBindToggleSlider('docListMarkerSize', { unit: 'px' }, (v) => patch({ marker_size: v }));
if (isPuces) {
document.getElementById('docListSvg').addEventListener('change', (e) => patch({ svg_markup: e.target.value.trim() }));
}
forgeDocBindBoxFields(panel, el, 'docList', patch);
forgeDocBindToggleSlider('docListItemPadding', { unit: 'px' }, (v) => patch({ item_padding: v }));
forgeDocBindToggleSlider('docListItemSpacing', { unit: 'px' }, (v) => patch({ item_spacing: v }));
}
function forgeDocRenderRowProps(panel, el) {
@@ -3635,16 +3778,26 @@ var forgeDocSelectedTemplateId = null;
function forgeDocRenderTemplateList() {
const list = document.getElementById('docTemplateList');
const themes = window.FORGE_DOCUMENT.themes || [];
if (!themes.length) {
list.innerHTML = '<div class="docEmptyState">Aucun modèle disponible pour le moment.</div>';
return;
}
list.innerHTML = themes.map((t) => `
// "Aucun modèle" (retour utilisateur du 26/09/2026 : la modale doit
// proposer de "revenir à un document de base") — une carte TOUJOURS
// présente, même si le catalogue est vide, avec un id sentinelle vide
// ("" est le themeId qu'attend /document/<slug>/theme/apply pour
// retirer le thème actuel, voir routes/document/document_theme_apply.py) —
// JAMAIS `null`, qui reste réservé à "rien n'a encore été cliqué"
// (voir forgeDocApplyTemplate, forgeDocOpenTemplateModal).
const noneCard = `
<button type="button" class="docTemplateCard ${forgeDocSelectedTemplateId === '' ? 'is-active' : ''}" data-theme-id="">
<div class="docTemplateCardName">Aucun modèle</div>
<div class="docTemplateCardCategory">Document sans mise en forme de modèle</div>
</button>
`;
const themeCards = themes.map((t) => `
<button type="button" class="docTemplateCard ${t.id === forgeDocSelectedTemplateId ? 'is-active' : ''}" data-theme-id="${t.id}">
<div class="docTemplateCardName">${forgeDocEscapeHtml(t.name)}</div>
<div class="docTemplateCardCategory">${forgeDocEscapeHtml(t.category)}</div>
</button>
`).join('');
list.innerHTML = noneCard + themeCards;
list.querySelectorAll('.docTemplateCard').forEach((btn) => {
btn.addEventListener('click', () => forgeDocSelectTemplate(btn.dataset.themeId));
});
@@ -3656,20 +3809,49 @@ function forgeDocSelectTemplate(themeId) {
const frame = document.getElementById('docTemplatePreviewFrame');
const empty = document.getElementById('docTemplatePreviewEmpty');
const actions = document.getElementById('docTemplatePreviewActions');
const keepBtn = document.getElementById('docTemplateKeepContentBtn');
const useBtn = document.getElementById('docTemplateUseContentBtn');
const removeBtn = document.getElementById('docTemplateRemoveBtn');
actions.hidden = false;
if (themeId === '') {
// "Aucun modèle" : rien à prévisualiser (aucun contenu de
// démonstration n'existe pour "pas de thème"), une seule action a
// du sens ici — retirer le thème actuel, jamais les deux boutons
// "contenu actuel/du modèle" qui supposent un vrai thème choisi.
frame.classList.remove('is-visible');
frame.src = 'about:blank';
empty.hidden = false;
empty.textContent = 'Revient à la mise en forme par défaut, sans modèle. Le contenu actuel du document n\'est jamais supprimé.';
keepBtn.hidden = true;
useBtn.hidden = true;
removeBtn.hidden = false;
return;
}
keepBtn.hidden = false;
useBtn.hidden = false;
removeBtn.hidden = true;
frame.src = `/document/${window.forgeDocState.slug}/theme/${themeId}/preview`;
frame.classList.add('is-visible');
empty.hidden = true;
actions.hidden = false;
}
function forgeDocApplyTemplate(mode) {
if (!forgeDocSelectedTemplateId) return;
// "" (Aucun modèle) est un choix explicite valide, jamais bloqué ici —
// seul `null` ("rien n'a encore été cliqué dans la modale") doit
// empêcher l'appel.
if (forgeDocSelectedTemplateId === null) return;
if (mode === 'template_content') {
const confirmed = window.confirm( // eslint-disable-line no-alert
'Remplacer tout le contenu actuel du document par le contenu de démonstration de ce modèle ? Cette action est irréversible.',
);
if (!confirmed) return;
}
if (forgeDocSelectedTemplateId === '') {
const confirmed = window.confirm( // eslint-disable-line no-alert
'Retirer le modèle actuel ? Le document revient à sa mise en forme par défaut (le contenu, lui, n\'est jamais supprimé).',
);
if (!confirmed) return;
}
fetch(`/document/${window.forgeDocState.slug}/theme/apply`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -3699,6 +3881,7 @@ function forgeDocOpenTemplateModal() {
frame.classList.remove('is-visible');
frame.src = 'about:blank';
empty.hidden = false;
empty.textContent = 'Sélectionnez un modèle pour le visualiser.';
actions.hidden = true;
}
@@ -3715,6 +3898,7 @@ function forgeDocBindTemplateModal() {
});
document.getElementById('docTemplateKeepContentBtn').addEventListener('click', () => forgeDocApplyTemplate('keep_content'));
document.getElementById('docTemplateUseContentBtn').addEventListener('click', () => forgeDocApplyTemplate('template_content'));
document.getElementById('docTemplateRemoveBtn').addEventListener('click', () => forgeDocApplyTemplate('keep_content'));
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
if (document.getElementById('docTemplateModal').classList.contains('is-open')) forgeDocCloseTemplateModal();
+28 -4
View File
@@ -96,11 +96,29 @@
height: 13px;
}
/* ---- Liste à puces / numérotée : coche sécurité / pastille panneau ---- */
/* ---- Liste à puces / numérotée : coche sécurité / pastille panneau ----
list-style:none supprime la puce/le numéro NATIF : ce thème affiche
son propre badge (::before ci-dessous) à la place, jamais les deux à
la fois (bug réel constaté le 26/09/2026 — sans ce reset, la feuille
de base affiche désormais aussi le marqueur natif en plus du badge,
voir static/document/document-editor.css, .docList li::before, qui a
dû arrêter de neutraliser le display natif du <li> pour corriger un
autre bug — retirer le thème faisait disparaître tout marqueur). ---- */
.docList {
font-family: var(--theme-font-body);
}
/* Sélecteur avec le type d'élément (ul.../ol...), pas seulement les
classes/attributs : static/style.css (site-wide) porte une règle
`.content ol:not([type]) { list-style-type: decimal; }` d'une
spécificité légèrement supérieure (elle inclut le sélecteur d'élément
`ol`) qui l'emportait sinon silencieusement sur ce reset, même si
celui-ci charge après. */
ul.docList[data-kind="liste_puces"],
ol.docList[data-kind="liste_numerotee"] {
list-style: none;
}
.docList li {
background: #faf7f2;
border: 1px solid var(--theme-line);
@@ -110,7 +128,6 @@
.docList[data-kind="liste_puces"] li::before {
content: "";
flex-shrink: 0;
width: 16px;
height: 16px;
margin-top: 1px;
@@ -126,10 +143,17 @@
.docList[data-kind="liste_numerotee"] li::before {
counter-increment: doc-theme-step;
content: counter(doc-theme-step);
flex-shrink: 0;
width: 22px;
height: 22px;
display: flex;
/* inline-flex, jamais flex : ce badge doit rester un ::before qui
s'écoule EN LIGNE à côté du texte de l'élément (voir
static/document/document-editor.css, .docList li::before) — flex
(sans inline-) le blockifie, ce qui le repousse au-dessus du texte
plutôt qu'à côté (bug réel constaté le 26/09/2026). inline-flex
garde ce comportement en ligne tout en centrant quand même le
chiffre à l'intérieur du badge (align-items/justify-content). */
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;