import html as html_lib from typing import Any, Callable # ---------- "❓ Boîte à quiz" — DEUX catégories de modèles bien distinctes # (voir game_engine/rendering/quiz_box_config.py) : # - "boîte de dialogue" (dialog_template, visibles SEULEMENT hors plein # écran) : "defaut" (Classique) + "manga_dialogue" (Manga), voir # render_quiz_box_manga_dialogue plus bas — "Verre dépoli"/"Néon"/ # "Minimal"/"Ludique" ont été retirés (demande explicite : "supprime # tous les modele de boite sauf le base") ; # - "page de quiz" (page_template, visibles SEULEMENT en plein écran, # voir render_quiz_box_page_classique/render_quiz_box_page_manga plus # bas) : thème complet et autonome, structure HTML totalement propre # à CHAQUE modèle, aucune structure partagée avec les modèles # "boîte" ni entre eux. # Chaque modèle a son propre bloc CSS (static/style.css) + éventuellement # son propre effet JS (static/game/js/play/quiz-box-templates.js), mais TOUS # doivent respecter le même "contrat" de points d'ancrage pour que le # moteur PARTAGÉ (static/game/js/play/dialogue-box-controller.js) fonctionne # quel que soit le modèle choisi : # - le conteneur racine porte class="quizBoxWidget[ quizBoxWidget--X]", # data-object-id/data-element-id, data-quiz-template, data-fullscreen, # data-timer-mode, data-timer-seconds (voir _quiz_data_attrs) ; # - [data-quiz-role="header"] / "question" / "choices" — JS y écrit # TOUJOURS via .textContent ou remplace .innerHTML en entier (jamais # un ajout) : ne JAMAIS poser de décoration (icône, badge, score...) # comme ENFANT de ces éléments précis, elle serait effacée au premier # rafraîchissement — un ::before CSS est la seule façon sûre d'ajouter # une icône à ces zones-là. C'est pourquoi le score/minuteur (voir # juste en dessous) vit dans un élément FRÈRE du header (jamais son # enfant), même si visuellement il doit apparaître à l'intérieur du # même bandeau (demande explicite : "le score doit être à l'intérieur # de la boîte à quiz, pas à l'extérieur") — voir _quiz_header_row_html, # qui les pose côte à côte dans un même bandeau partageant le même # fond, plutôt qu'en deux bandes empilées de couleurs différentes ; # - [data-quiz-role="score"]/[data-quiz-role="timer"] — un modèle # "boîte" les regroupe dans un [data-quiz-role="topbar"] (masqué hors # plein écran par static/style.css) ; un modèle "page" n'a pas besoin # de ce regroupement/masquage (il n'existe QUE plein écran) et peut # les placer où il veut (voir render_quiz_box_page_manga) — le moteur # JS ne cherche jamais "topbar" lui-même, seulement score/timer. # "defaut" (Classique) reste visuellement identique au style déjà en # place avant cette extension (pas de suffixe de classe) — voir # render_quiz_box_defaut. def _quiz_data_attrs(obj: dict[str, Any], config: dict[str, Any], active_template: str) -> str: return ( f'data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" ' f'data-quiz-template="{active_template}" ' f'data-fullscreen="{"1" if config["fullscreen"] else "0"}" ' f'data-timer-mode="{config["timer_mode"]}" data-timer-seconds="{config["timer_seconds"]}"' ) def _quiz_box_style(obj: dict[str, Any], style: dict[str, Any]) -> str: return ( f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; " f"width:{obj['width']}px; height:{obj['height']}px; z-index:{obj['z_index']}; " f"font-family:{html_lib.escape(style['font_family'])}; font-size:{style['font_size']}px; " f"font-weight:{style['font_weight']}; color:{html_lib.escape(style['text_color'])};" ) def _quiz_topbar_html() -> str: return ( '
La question s\'affiche ici.
' f'La question s\'affiche ici.
' f'