diff --git a/screens/rendering/render_checkbox_or_radio.py b/screens/rendering/render_checkbox_or_radio.py index 63b8eb04..e97eddd8 100644 --- a/screens/rendering/render_checkbox_or_radio.py +++ b/screens/rendering/render_checkbox_or_radio.py @@ -12,4 +12,8 @@ def _render_checkbox_or_radio(el, meta, slug=None, children_map=None, ctx=None, attrs["type"] = input_type style = _style_string(el, " display:flex; align-items:center; gap:8px;", parent_flex_direction) content = _apply_ctx(el.get("content") or "", ctx) - return f' {html_lib.escape(content)}' + # entourant l'input EST la façon dont + # Bulma stylise nativement une case à cocher/un bouton radio — pas de + # wrapper supplémentaire nécessaire, la structure existante correspondait + # déjà exactement à cette convention. + return f' {html_lib.escape(content)}' diff --git a/screens/rendering/render_jauge.py b/screens/rendering/render_jauge.py index 3639e0e0..f2efc3d4 100644 --- a/screens/rendering/render_jauge.py +++ b/screens/rendering/render_jauge.py @@ -127,14 +127,21 @@ def _render_jauge(el, meta, slug, children_map=None, ctx=None, parent_flex_direc f'color:#e8eaf0; text-align:{text_align};">{html_lib.escape(str(nom_value))}' ) - # La classe "jaugeBar" est repérée côté client (play.html, refreshRuntimeData) - # pour savoir que ce wrapper doit être régénéré après toute action - # "Modifier une donnée" — sinon la jauge resterait figée sur sa valeur - # de départ après un changement, exactement comme il fallait déjà - # repérer "repeaterItem" pour les Répéteurs (voir 1.1/1.2). + # Vraie balise Bulma ("class=progress") plutôt qu'une barre + # maison faite de positionnés — la couleur dégradée bas/haut (voir + # _interpolate_color ci-dessus) reste possible nativement : Bulma pilote + # justement le remplissage via la variable CSS --bulma-progress-value- + # background-color, qu'on peut fixer en ligne à la couleur calculée sans + # sortir du système de classes de Bulma. La classe "jaugeBar" est + # repérée côté client (play.html, refreshRuntimeData) pour savoir que + # cette balise doit être régénérée après toute action "Modifier une + # donnée" — sinon la jauge resterait figée sur sa valeur de départ après + # un changement, exactement comme il fallait déjà repérer "repeaterItem" + # pour les Répéteurs (voir 1.1/1.2). bar = ( - f'' + f'{percent}%' ) # min-height (pas 0) : un élément posé DANS un conteneur a une hauteur # "auto" par défaut (voir _style_string) — "flex:1 1 auto" seul n'a @@ -145,7 +152,7 @@ def _render_jauge(el, meta, slug, children_map=None, ctx=None, parent_flex_direc # l'élément a une vraie hauteur (posé directement sur un écran, ou # hauteur fixée via "Position & taille"). bar_wrap = ( - f'{bar}{label_html}' ) return ( diff --git a/screens/rendering/render_onglets.py b/screens/rendering/render_onglets.py index 02e1262f..633b933e 100644 --- a/screens/rendering/render_onglets.py +++ b/screens/rendering/render_onglets.py @@ -38,10 +38,14 @@ def _render_onglets(el, meta, slug, children_map=None, ctx=None, parent_flex_dir active = i == 0 tab_html = render_element_html(tab, slug, children_map, ctx, parent_flex_direction=flex_dir) target_id = f"tabpanel-{tab['id']}" + # Bulma natif ("tabs" > "ul" > "li", "is-active" sur le — + # pas le — pour l'onglet en cours) : le porte le clic ET les + # data-attributes ciblés par forgeShowTab (voir screen_edit.html / + # play.html), le imbriqué n'est là que pour le rendu visuel Bulma. bar_items.append( - f'{html_lib.escape(str(label))}' + f'onclick="forgeShowTab(this)">{html_lib.escape(str(label))}' ) hidden_style = "" if active else ' style="display:none;"' panels.append( @@ -51,7 +55,7 @@ def _render_onglets(el, meta, slug, children_map=None, ctx=None, parent_flex_dir return ( f'' - f'{"".join(bar_items)}' + f'{"".join(bar_items)}' f'{"".join(panels)}' f"" ) diff --git a/screens/rendering/render_select.py b/screens/rendering/render_select.py index d46eafc7..cd3835c1 100644 --- a/screens/rendering/render_select.py +++ b/screens/rendering/render_select.py @@ -4,12 +4,22 @@ from .visible_attrs import _visible_attrs from .apply_ctx import _apply_ctx from .style_string import _style_string from .attr_string import _attr_string +from .auto_id import _auto_id def _render_select(el, meta, slug=None, children_map=None, ctx=None, parent_flex_direction=None): - attrs = _visible_attrs(el, meta, ctx) - style = _style_string(el, parent_flex_direction=parent_flex_direction) + # Bulma stylise un via la classe "select" posée sur un + # QUI L'ENTOURE (le petit triangle est un ::after de ce wrapper) — la + # poser directement sur la balise elle-même n'aurait aucun + # effet. Le wrapper porte donc l'id/positionnement habituel de + # l'élément (comme tous les autres widgets), le reste à + # l'intérieur avec ses propres attributs de formulaire (name, required). + outer_attrs = _visible_attrs(el, meta, ctx) + select_attrs = {k: v for k, v in outer_attrs.items() if k not in ("id", "class", "data-element-id", "data-anim-target")} + wrapper_attrs = {k: v for k, v in outer_attrs.items() if k in ("id", "data-element-id", "data-anim-target")} + style = _style_string(el, " display:flex;", parent_flex_direction) content = _apply_ctx(el.get("content") or "", ctx) options = [o.strip() for o in content.split("\n") if o.strip()] opts_html = "".join(f'{html_lib.escape(o)}' for o in options) - return f'{opts_html}' + select_html = f'{opts_html}' + return f'{select_html}' diff --git a/screens/rendering/render_table.py b/screens/rendering/render_table.py index 689f26b7..a7daee90 100644 --- a/screens/rendering/render_table.py +++ b/screens/rendering/render_table.py @@ -10,15 +10,12 @@ def _render_table(el, meta, slug=None, children_map=None, ctx=None, parent_flex_ attrs = _visible_attrs(el, meta, ctx) header_row = (el.get("attributes") or {}).get("_header_row") in ("1", "true", "on") content = _apply_ctx(el.get("content") or "", ctx) - style = _style_string(el, " border-collapse:collapse;", parent_flex_direction) + style = _style_string(el, "", parent_flex_direction) rows = [r for r in content.split("\n") if r.strip()] html_rows = [] for i, r in enumerate(rows): cells = r.split(";") cell_tag = "th" if (header_row and i == 0) else "td" - cells_html = "".join( - f'<{cell_tag} style="border:1px solid #2b3140; padding:6px 8px;">{html_lib.escape(c.strip())}{cell_tag}>' - for c in cells - ) + cells_html = "".join(f"<{cell_tag}>{html_lib.escape(c.strip())}{cell_tag}>" for c in cells) html_rows.append(f"{cells_html}") return f'{"".join(html_rows)}' diff --git a/screens/widgets/registry.py b/screens/widgets/registry.py index 01ca27dd..998be7d3 100644 --- a/screens/widgets/registry.py +++ b/screens/widgets/registry.py @@ -100,7 +100,7 @@ WIDGETS = { }, "champ_texte": { "label": "Champ — texte", "icon": "⌨️", "tag": "input", "has_content": False, "self_closing": True, - "w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "text"}, + "w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "text", "class": "input"}, "controls": [ c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Votre réponse", group="Contenu"), c_text("name", "Nom du champ", "attr:name", placeholder="ex. reponse1", group="Contenu"), @@ -109,7 +109,7 @@ WIDGETS = { }, "champ_email": { "label": "Champ — email", "icon": "📧", "tag": "input", "has_content": False, "self_closing": True, - "w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "email"}, + "w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "email", "class": "input"}, "controls": [ c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. votre@email.com", default="votre@email.com", group="Contenu"), c_text("name", "Nom du champ", "attr:name", placeholder="ex. email", group="Contenu"), @@ -118,7 +118,7 @@ WIDGETS = { }, "champ_mot_de_passe": { "label": "Champ — mot de passe", "icon": "🔒", "tag": "input", "has_content": False, "self_closing": True, - "w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "password"}, + "w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "password", "class": "input"}, "controls": [ c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Mot de passe", group="Contenu"), c_text("name", "Nom du champ", "attr:name", placeholder="ex. mot_de_passe", group="Contenu"), @@ -149,7 +149,7 @@ WIDGETS = { }, "zone_texte": { "label": "Zone de texte", "icon": "🗒️", "tag": "textarea", "has_content": False, - "w": 40, "h": 18, "default_content": "", + "w": 40, "h": 18, "default_content": "", "fixed_attrs": {"class": "textarea"}, "controls": [ c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Votre message", group="Contenu"), c_text("name", "Nom du champ", "attr:name", placeholder="ex. message", group="Contenu"), @@ -195,6 +195,7 @@ WIDGETS = { "tableau": { "label": "Tableau", "icon": "🗂️", "tag": "table", "has_content": True, "special_render": "table", "w": 55, "h": 25, "default_content": "Colonne 1;Colonne 2\nValeur A;Valeur B", + "fixed_attrs": {"class": "table is-bordered is-fullwidth"}, "controls": [ c_textarea("content", "Contenu — une ligne par rangée, colonnes séparées par ;", "content", default="Colonne 1;Colonne 2\nValeur A;Valeur B", group="Contenu"), diff --git a/static/style.css b/static/style.css index 9bd2ad87..d50208ef 100644 --- a/static/style.css +++ b/static/style.css @@ -254,14 +254,9 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d } .contextMenu button:hover{ background:var(--panel2); } -/* Élément à onglets (voir render_onglets.py) */ -.forgeTabBar{ display:flex; flex-wrap:wrap; gap:2px; border-bottom:1px solid var(--border); } -.forgeTabBtn{ - background:none; border:none; border-bottom:2px solid transparent; color:var(--text-dim); - padding:8px 14px; font-size:14px; font-family:inherit; cursor:pointer; white-space:nowrap; -} -.forgeTabBtn:hover{ color:var(--text); } -.forgeTabBtn.active{ color:var(--text); border-bottom-color:var(--accent); font-weight:600; } +/* Élément à onglets (voir render_onglets.py) : la barre d'onglets elle-même + est un vrai composant Bulma ("tabs" > "ul" > "li", voir render_onglets.py) + — il ne reste ici que les panneaux, que Bulma n'a pas de composant pour. */ .forgeTabPanels{ min-width:0; } .forgeTabPanel{ padding-top:10px; } diff --git a/templates/play.html b/templates/play.html index ac51625c..940b0497 100644 --- a/templates/play.html +++ b/templates/play.html @@ -82,7 +82,7 @@ var groupId = btn.dataset.tabBtn; var targetId = btn.dataset.tabTarget; document.querySelectorAll('[data-tab-btn="' + groupId + '"]').forEach(function (b) { - b.classList.toggle('active', b === btn); + b.classList.toggle('is-active', b === btn); }); document.querySelectorAll('[data-tab-group="' + groupId + '"]').forEach(function (p) { p.style.display = (p.id === targetId) ? '' : 'none'; diff --git a/templates/screen_edit.html b/templates/screen_edit.html index 260bbc6c..dbdb503f 100644 --- a/templates/screen_edit.html +++ b/templates/screen_edit.html @@ -758,7 +758,7 @@ function forgeShowTab(btn) { var groupId = btn.dataset.tabBtn; var targetId = btn.dataset.tabTarget; document.querySelectorAll('[data-tab-btn="' + groupId + '"]').forEach(function (b) { - b.classList.toggle('active', b === btn); + b.classList.toggle('is-active', b === btn); }); document.querySelectorAll('[data-tab-group="' + groupId + '"]').forEach(function (p) { p.style.display = (p.id === targetId) ? '' : 'none'; diff --git a/tests/test_jauge.py b/tests/test_jauge.py index 5dbd6dfe..216286ef 100644 --- a/tests/test_jauge.py +++ b/tests/test_jauge.py @@ -33,7 +33,7 @@ def test_gauge_width_reflects_field_value_without_extra_action(client, game): }) html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode() - assert "width:50.0%" in html + assert 'value="50.0"' in html def test_gauge_updates_after_data_change_with_no_extra_wiring(client, game): @@ -52,7 +52,7 @@ def test_gauge_updates_after_data_change_with_no_extra_wiring(client, game): client.post(f"/game/{game}/objects/{def_id}/data/new", data={"reputation": "90"}) html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode() - assert "width:90.0%" in html + assert 'value="90.0"' in html def test_gauge_color_gradient_shifts_between_low_and_high(client, game): @@ -67,7 +67,7 @@ def test_gauge_color_gradient_shifts_between_low_and_high(client, game): "ctrl_couleur_basse": "#ff0000", "ctrl_couleur_haute": "#00ff00", }) html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode() - assert "background-color:#00ff00" in html # valeur au max -> couleur "haute" pure + assert "--bulma-progress-value-background-color:#00ff00" in html # valeur au max -> couleur "haute" pure def test_gauge_value_clamped_visually_when_out_of_range(client, game): @@ -82,7 +82,7 @@ def test_gauge_value_clamped_visually_when_out_of_range(client, game): "ctrl_definition_id": str(def_id), "ctrl_champ": "reputation", "ctrl_min": "0", "ctrl_max": "100", }) html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode() - assert "width:100.0%" in html + assert 'value="100.0"' in html # ---------- Choix de l'enregistrement (plusieurs jauges dans un même objet) @@ -121,7 +121,7 @@ def test_gauge_row_id_distinguishes_between_records_of_the_same_object(client, g "ctrl_champ": "value", "ctrl_min": "0", "ctrl_max": "100", }) html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode() - assert "width:80.0%" in html + assert 'value="80.0"' in html def test_gauge_without_row_id_falls_back_to_latest_row_no_regression(client, game): @@ -136,7 +136,7 @@ def test_gauge_without_row_id_falls_back_to_latest_row_no_regression(client, gam "ctrl_definition_id": str(def_id), "ctrl_champ": "value", "ctrl_min": "0", "ctrl_max": "100", }) html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode() - assert "width:80.0%" in html # "Confiance", ajoutée en dernier + assert 'value="80.0"' in html # "Confiance", ajoutée en dernier def test_gauge_name_field_renders_above_the_bar(client, game): @@ -186,7 +186,8 @@ def test_gauge_bar_stays_visible_when_nested_in_a_container_with_a_name(client, html = client.get(f"/game/{game}/play").data.decode() assert ">Confiance<" in html idx = html.find("jaugeBar") - wrap_style = html[html.rfind('style="', 0, html.rfind('', div_start)] assert "min-height:14px" in wrap_style diff --git a/tests/test_onglets_widget.py b/tests/test_onglets_widget.py index b4b825c9..34b37ecd 100644 --- a/tests/test_onglets_widget.py +++ b/tests/test_onglets_widget.py @@ -88,7 +88,7 @@ def test_deleting_a_tab_removes_it_and_reselects_the_tabs_widget(client, game): assert f"selected={onglets_id}" in resp.headers["Location"] html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode() - assert html.count("forgeTabBtn") == 1 + assert html.count("forgeShowTab(this)") == 1 def test_tab_content_holds_a_real_container_children_can_be_added_to(client, game):