Ajoute une largeur maximale optionnelle aux blocs de texte (titre/paragraphe)
Nouvel attribut max_width (vide par défaut = pleine largeur, inchangé) sur les kinds titre/paragraphe, réglable depuis leur panneau Propriétés. Le sous-titre de la page de garde du thème Sécurité Incendie l'utilise (60ch) pour rester conforme à la maquette d'origine, qui ne l'étirait pas sur toute la largeur de la page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
746feb3796
commit
1296edc2f8
@@ -46,6 +46,10 @@ _TEXT_DEFAULTS = {
|
|||||||
"underline": False,
|
"underline": False,
|
||||||
"align": "left",
|
"align": "left",
|
||||||
"color": "var(--forge-text)",
|
"color": "var(--forge-text)",
|
||||||
|
# Vide par défaut = pleine largeur de .docPageContent (comportement
|
||||||
|
# historique) ; une valeur CSS libre (ex. "60ch", "480px") restreint
|
||||||
|
# la largeur du bloc — voir render_document_element._render_text.
|
||||||
|
"max_width": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ l'affichage/debug hors bibliothèque).
|
|||||||
Attributs posés à la création d'un élément de ce type (voir
|
Attributs posés à la création d'un élément de ce type (voir
|
||||||
`document_engine/elements/add_document_element.py`).
|
`document_engine/elements/add_document_element.py`).
|
||||||
- **Retour** : un dict d'attributs par défaut, dépendant du `kind` :
|
- **Retour** : un dict d'attributs par défaut, dépendant du `kind` :
|
||||||
texte (`content/style` + `bold/italic/underline/align/color`),
|
texte (`content/style` + `bold/italic/underline/align/color/max_width` —
|
||||||
|
`max_width` vide par défaut = pleine largeur de `.docPageContent`,
|
||||||
|
une valeur CSS libre ex. `"60ch"`/`"480px"` restreint le bloc, voir
|
||||||
|
`rendering.md`),
|
||||||
image (`src/alt/svg_markup` — `svg_markup` prend le pas sur `src` au
|
image (`src/alt/svg_markup` — `svg_markup` prend le pas sur `src` au
|
||||||
rendu, voir rendering.md), bouton (`label/target/
|
rendu, voir rendering.md), bouton (`label/target/
|
||||||
attachment_stored_name/attachment_filename` — la pièce jointe est
|
attachment_stored_name/attachment_filename` — la pièce jointe est
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ def _render_text(el: dict[str, Any], _children_by_parent: dict[int | None, list[
|
|||||||
f"font-size:{font_size}; font-weight:{weight}; line-height:{line_height}; "
|
f"font-size:{font_size}; font-weight:{weight}; line-height:{line_height}; "
|
||||||
f"font-style:{font_style}; text-decoration:{text_decoration}; text-align:{align}; color:{color};"
|
f"font-style:{font_style}; text-decoration:{text_decoration}; text-align:{align}; color:{color};"
|
||||||
)
|
)
|
||||||
|
# max_width optionnel (ex. "60ch", "480px") — vide par défaut (pleine
|
||||||
|
# largeur de .docPageContent, comportement inchangé). Retour
|
||||||
|
# utilisateur du 24/09/2026 : un paragraphe doit pouvoir rester plus
|
||||||
|
# étroit que la page, comme un sous-titre sous un grand titre, sans
|
||||||
|
# dépendre d'une rangée (qui partagerait la largeur avec un frère).
|
||||||
|
max_width = str(a.get("max_width", "")).strip()
|
||||||
|
if max_width:
|
||||||
|
style += f" max-width:{html_lib.escape(max_width)};"
|
||||||
return f'<div class="docText" data-element-id="{el["id"]}" data-kind="{el["kind"]}" style="{style}">{content}</div>'
|
return f'<div class="docText" data-element-id="{el["id"]}" data-kind="{el["kind"]}" style="{style}">{content}</div>'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ regroupement à chaque appel.
|
|||||||
récursivement.
|
récursivement.
|
||||||
- **Texte** (`titre`/`paragraphe`) : `<div>` stylé selon `style` (préréglage
|
- **Texte** (`titre`/`paragraphe`) : `<div>` stylé selon `style` (préréglage
|
||||||
taille/graisse/interligne) et `bold`/`italic`/`underline`/`align`/`color`.
|
taille/graisse/interligne) et `bold`/`italic`/`underline`/`align`/`color`.
|
||||||
|
`max_width` (optionnel, ex. `"60ch"`, `"480px"`) ajoute `max-width` au
|
||||||
|
style inline quand non vide — pleine largeur de `.docPageContent` par
|
||||||
|
défaut, retour utilisateur du 24/09/2026 (un paragraphe doit pouvoir
|
||||||
|
rester plus étroit que la page, sans dépendre d'une rangée qui en
|
||||||
|
partagerait la largeur avec un frère).
|
||||||
- **Image** : `<img>`, ou un bloc placeholder si `src` est vide — OU, si
|
- **Image** : `<img>`, ou un bloc placeholder si `src` est vide — OU, si
|
||||||
`attributes["svg_markup"]` est non vide (prioritaire sur `src`), un
|
`attributes["svg_markup"]` est non vide (prioritaire sur `src`), un
|
||||||
`<div>` portant directement ce fragment SVG nettoyé par
|
`<div>` portant directement ce fragment SVG nettoyé par
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ SECURITE_INCENDIE_SEED_PAGES: list[dict[str, Any]] = [
|
|||||||
"content": (
|
"content": (
|
||||||
"Reconnaître les risques, réagir dans les 3 premières minutes, protéger les "
|
"Reconnaître les risques, réagir dans les 3 premières minutes, protéger les "
|
||||||
"personnes autour de vous. Durée : 25 min · à renouveler tous les 24 mois."
|
"personnes autour de vous. Durée : 25 min · à renouveler tous les 24 mois."
|
||||||
)
|
),
|
||||||
|
"max_width": "60ch",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1720,12 +1720,14 @@ function forgeDocRenderTextProps(panel, el) {
|
|||||||
${['var(--doc-text)', 'var(--doc-muted)', '#ff5f2e', '#ffb020'].map((c) => `<div class="docSwatch ${a.color === c ? 'is-active' : ''}" data-color="${c}" style="background:${c.replace('doc-', 'forge-').replace('var(--forge-text)', 'var(--forge-text)')};"></div>`).join('')}
|
${['var(--doc-text)', 'var(--doc-muted)', '#ff5f2e', '#ffb020'].map((c) => `<div class="docSwatch ${a.color === c ? 'is-active' : ''}" data-color="${c}" style="background:${c.replace('doc-', 'forge-').replace('var(--forge-text)', 'var(--forge-text)')};"></div>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="docField"><label for="docMaxWidthInput">Largeur maximale (optionnel, ex. 60ch, 480px)</label><input type="text" id="docMaxWidthInput" value="${a.max_width || ''}" placeholder="pleine largeur"></div>
|
||||||
${forgeDocDeleteButtonHtml()}
|
${forgeDocDeleteButtonHtml()}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
|
function patch(partial) { forgeDocUpdateAttributes(el.id, { ...el.attributes, ...partial }); }
|
||||||
|
|
||||||
document.getElementById('docContentInput').addEventListener('change', (e) => patch({ content: e.target.value }));
|
document.getElementById('docContentInput').addEventListener('change', (e) => patch({ content: e.target.value }));
|
||||||
|
document.getElementById('docMaxWidthInput').addEventListener('change', (e) => patch({ max_width: e.target.value.trim() }));
|
||||||
panel.querySelectorAll('#docStyleSeg .docSegBtn').forEach((btn) => {
|
panel.querySelectorAll('#docStyleSeg .docSegBtn').forEach((btn) => {
|
||||||
btn.addEventListener('click', () => patch({ style: btn.dataset.style }));
|
btn.addEventListener('click', () => patch({ style: btn.dataset.style }));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -400,3 +400,36 @@ def test_bouton_with_attachment_filename_renders_data_attribute(tmp_support_slug
|
|||||||
|
|
||||||
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
|
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
|
||||||
assert 'data-attachment-filename="fiche-consignes.pdf"' in html
|
assert 'data-attachment-filename="fiche-consignes.pdf"' in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_paragraphe_default_max_width_is_empty(tmp_support_slug_cleanup: Any) -> None:
|
||||||
|
slug, page_id = _new_support(tmp_support_slug_cleanup)
|
||||||
|
element_id = doc_engine.add_document_element(slug, "paragraphe", page_id=page_id)
|
||||||
|
element = doc_engine.get_document_element(slug, element_id)
|
||||||
|
assert element is not None
|
||||||
|
assert element["attributes"]["max_width"] == ""
|
||||||
|
|
||||||
|
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
|
||||||
|
assert "max-width" not in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_paragraphe_with_max_width_adds_it_to_the_inline_style(tmp_support_slug_cleanup: Any) -> None:
|
||||||
|
slug, page_id = _new_support(tmp_support_slug_cleanup)
|
||||||
|
element_id = doc_engine.add_document_element(slug, "paragraphe", page_id=page_id)
|
||||||
|
doc_engine.update_document_element_attributes(
|
||||||
|
slug, element_id, {"content": "Texte", "style": "paragraphe", "max_width": "60ch"}
|
||||||
|
)
|
||||||
|
|
||||||
|
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
|
||||||
|
assert "max-width:60ch;" in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_paragraphe_max_width_is_escaped(tmp_support_slug_cleanup: Any) -> None:
|
||||||
|
slug, page_id = _new_support(tmp_support_slug_cleanup)
|
||||||
|
element_id = doc_engine.add_document_element(slug, "paragraphe", page_id=page_id)
|
||||||
|
doc_engine.update_document_element_attributes(
|
||||||
|
slug, element_id, {"content": "Texte", "style": "paragraphe", "max_width": '"><script>alert(1)</script>'}
|
||||||
|
)
|
||||||
|
|
||||||
|
html = doc_engine.render_document(doc_engine.list_document_elements(slug, page_id))
|
||||||
|
assert "<script>" not in html
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ def test_securite_incendie_title_page_is_vertically_centered() -> None:
|
|||||||
assert entry["seed_pages"][1].get("vertical_align", "top") == "top"
|
assert entry["seed_pages"][1].get("vertical_align", "top") == "top"
|
||||||
|
|
||||||
|
|
||||||
|
def test_securite_incendie_intro_paragraph_has_a_max_width() -> None:
|
||||||
|
"""Retour utilisateur du 24/09/2026 : le sous-titre de la page de
|
||||||
|
garde doit rester aussi étroit que dans la maquette, pas étiré sur
|
||||||
|
toute la largeur de la page."""
|
||||||
|
entry = doc_engine.get_document_theme_entry("securite-incendie")
|
||||||
|
assert entry is not None
|
||||||
|
intro_paragraph = entry["seed_pages"][0]["blocks"][2]
|
||||||
|
assert intro_paragraph["kind"] == "paragraphe"
|
||||||
|
assert intro_paragraph["attributes"]["max_width"] == "60ch"
|
||||||
|
|
||||||
|
|
||||||
def test_seed_blocks_to_elements_assigns_unique_synthetic_ids() -> None:
|
def test_seed_blocks_to_elements_assigns_unique_synthetic_ids() -> None:
|
||||||
blocks: list[dict[str, Any]] = [
|
blocks: list[dict[str, Any]] = [
|
||||||
{"kind": "titre", "attributes": {"content": "Titre"}},
|
{"kind": "titre", "attributes": {"content": "Titre"}},
|
||||||
|
|||||||
Reference in New Issue
Block a user