From d86b1a60c7333418e08463d663aa0cf30ed6fd7b Mon Sep 17 00:00:00 2001 From: william Date: Sun, 23 Aug 2026 11:00:09 +0200 Subject: [PATCH 01/62] Fix builder canvas resizing on element selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main.content (.content-builder) inherited "margin:0 auto" from the base .content class. On this page body is a column flex container, so that auto margin disabled cross-axis stretch and made the element shrink-to-fit instead — sized via an algorithm that runs back down through the flex/%/aspect-ratio chain to the canvas, and shifts slightly whenever sibling panel content changes (e.g. selecting an element). Give it an explicit width:100% so max-width/auto-margin centering still work but sizing is stable and fills the available space. Also add min-width:0 to .builderPanelRight, matching its siblings, as defense against the same class of bug. Verified with Playwright: canvas box was previously not filling available width (918px expected vs 379px actual at a 1920px viewport) and shifted between steps; both are fixed. --- static/style.css | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/static/style.css b/static/style.css index 674c0dd2..4d46fd8d 100644 --- a/static/style.css +++ b/static/style.css @@ -84,7 +84,21 @@ body.builderBody{ display:flex; flex-direction:column; height:100vh; margin:0; o body.builderBody > .topbar{ flex:0 0 auto; } body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; display:flex; flex-direction:column; } -.content-builder{ max-width:1600px; } +.content-builder{ + /* .content (ci-dessus) pose "margin:0 auto" pour centrer une page normale + — mais sur cette page, le body est un flex-container en colonne + (body.builderBody), et une marge horizontale "auto" y désactive le + stretch d'un flex-item : sa largeur est alors recalculée en + "shrink-to-fit" à partir de son contenu, un calcul qui traverse toute + la chaîne flex/%/aspect-ratio jusqu'au canvas et redonne un résultat + légèrement différent à chaque changement de contenu du panneau (élément + sélectionné, grille affichée...) — d'où le canvas qui grandit/rétrécit + sans raison apparente à chaque interaction. "width:100%" donne une + taille définie à la place : la marge "auto" ne fait alors que centrer + l'espace restant une fois "max-width" atteint, comme voulu à l'origine. */ + width:100%; + max-width:1600px; +} .content-builder > .fieldRow:first-child{ flex:0 0 auto; } /* 3 colonnes : ajout d'éléments (gauche) / écran en grand (centre) / propriétés (droite) — @@ -92,7 +106,7 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d disponible), avec leur propre défilement interne si leur contenu dépasse. */ .builder3{ display:flex; gap:20px; align-items:stretch; margin-top:14px; flex:1 1 auto; min-height:0; } .builderPanel{ flex:0 0 260px; min-width:0; height:100%; overflow-y:auto; } -.builderPanelRight{ flex:0 0 320px; height:100%; overflow-y:auto; } +.builderPanelRight{ flex:0 0 320px; min-width:0; height:100%; overflow-y:auto; } .builderCanvasArea{ flex:1 1 auto; min-width:0; height:100%; overflow-y:auto; display:flex; flex-direction:column; } .widgetGrid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; } From 4a593d07e43051ccff78c6606cb73fa6db678b0a Mon Sep 17 00:00:00 2001 From: william Date: Sun, 23 Aug 2026 12:14:00 +0200 Subject: [PATCH 02/62] Consolidate the properties panel: Position & taille, no more Taille/Survol/duplicate Disposition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge "Taille dans le conteneur" into "Position & taille": a child element (posé dans un conteneur/répéteur/groupe de champs) now shows greyed-out X/Y (not applicable, it follows its parent's layout) and a single Largeur/Hauteur field per axis with a unit selector (%/px) instead of the old fixed-px-only sliders capped at 1000 — which was the root cause of a landscape-mode bug where a child couldn't be made wider than 1000px even though the actual screen was wider. New c_size control type (one style key per axis, not two competing sliders — an earlier px+% two-slider attempt let the untouched slider silently clobber the other's value on every autosave). - Merge the two "Disposition" groups (visibility + scale, previously split apart in UNIVERSAL_CONTROLS by unrelated groups) into one. - Remove the "Survol" panel: hovering is conceptually a flow trigger, not a static element property — to be reintroduced there. The underlying data-hover-text/bindHoverTexts runtime is untouched. --- screens/elements/save_element_controls.py | 19 +++++++++ .../control_groups/size_override_controls.py | 39 +++++++++++------ .../control_groups/universal_controls.py | 10 ++++- screens/widgets/control_value.py | 8 ++++ screens/widgets/controls/c_size.py | 5 +++ screens/widgets/default_style_for_widget.py | 16 +++---- static/style.css | 4 ++ templates/screen_edit.html | 42 ++++++++++++++++--- tests/test_confort.py | 10 ++++- 9 files changed, 120 insertions(+), 33 deletions(-) create mode 100644 screens/widgets/controls/c_size.py diff --git a/screens/elements/save_element_controls.py b/screens/elements/save_element_controls.py index 7df352b4..6e3d3842 100644 --- a/screens/elements/save_element_controls.py +++ b/screens/elements/save_element_controls.py @@ -64,6 +64,25 @@ def save_element_controls(slug, element_id, form): style["transform"] = f"scale({val})" continue + if ctype == "size": + # Un seul champ CSS ("width"/"height") mais deux entrées de + # formulaire (valeur + unité) — voir size_override_controls.py + # pour pourquoi ce n'est PAS deux contrôles slider séparés. + _kind, name = control["target"].split(":", 1) + try: + num = float(form.get(field_name + "_value") or 0) + except (TypeError, ValueError): + num = 0.0 + unit = form.get(field_name + "_unit", "%") + if unit not in ("px", "%"): + unit = "%" + if num <= 0: + style.pop(name, None) + else: + num_str = str(int(num)) if num == int(num) else str(num) + style[name] = f"{num_str}{unit}" + continue + target = control["target"] if ctype == "checkbox": checked = form.get(field_name) is not None diff --git a/screens/widgets/control_groups/size_override_controls.py b/screens/widgets/control_groups/size_override_controls.py index b46e53f8..8329efd6 100644 --- a/screens/widgets/control_groups/size_override_controls.py +++ b/screens/widgets/control_groups/size_override_controls.py @@ -1,16 +1,29 @@ -from ..controls.c_slider import c_slider +from ..controls.c_size import c_size -# Taille fixe (en pixels), surtout utile pour un élément posé À L'INTÉRIEUR -# d'un conteneur/répéteur/groupe de champs : par défaut, ces éléments -# remplissent la largeur disponible et s'ajustent en hauteur à leur contenu -# (voir _style_string) — 0 garde ce comportement automatique ; une valeur -# non nulle impose une taille fixe (ex: une image de 120px de large dans une -# ligne, pour que "Espacement égal entre eux"/"autour"/"uniforme" sur le -# conteneur parent ait un effet visible, ce qui n'est pas le cas tant que -# chaque élément remplit 100% de la largeur). +# Taille d'un élément posé À L'INTÉRIEUR d'un conteneur/répéteur/groupe de +# champs : par défaut, ces éléments remplissent la largeur disponible et +# s'ajustent en hauteur à leur contenu (voir _style_string) — 0 garde ce +# comportement automatique ; une valeur non nulle impose une taille fixe, +# dans l'unité choisie par l'utilisateur (% du parent, qui suit +# naturellement n'importe quelle taille d'écran — ou px, pour une taille +# précise, ex: une image de 120px de large dans une ligne, pour que +# "Espacement égal entre eux"/"autour"/"uniforme" sur le conteneur parent +# ait un effet visible, ce qui n'est pas le cas tant que chaque élément +# remplit 100% de la largeur). +# +# UN SEUL contrôle par axe (pas un slider % + un slider px séparés) : les +# deux cibleraient la même propriété CSS "width"/"height", et le mécanisme +# de save_element_controls.py qui préserve un réglage "jamais personnalisé" +# ne protège que le PREMIER contrôle à écrire une clé donnée — un second +# contrôle non touché par l'utilisateur mais ciblant la même clé +# l'écraserait quand même à chaque sauvegarde (bug constaté en pratique). +# +# Ces réglages ne sont PAS rendus via le mécanisme générique de groupes du +# panneau de propriétés (voir screen_edit.html) : ils sont affichés à la +# main dans le bloc "Position & taille", à la place de X/Y (non applicables +# à un élément qui suit le flux de son parent plutôt qu'une position +# absolue) — voir selected_controls / size_ctrls dans le template. SIZE_OVERRIDE_CONTROLS = [ - c_slider("child-width", "Largeur fixe", "style:width", - min=0, max=1000, default=0, group="Taille dans le conteneur"), - c_slider("child-height", "Hauteur fixe", "style:height", - min=0, max=1000, default=0, group="Taille dans le conteneur"), + c_size("child-width", "Largeur", "style:width", default=0, group="Position & taille"), + c_size("child-height", "Hauteur", "style:height", default=0, group="Position & taille"), ] diff --git a/screens/widgets/control_groups/universal_controls.py b/screens/widgets/control_groups/universal_controls.py index c18bfb3d..2dcc8d68 100644 --- a/screens/widgets/control_groups/universal_controls.py +++ b/screens/widgets/control_groups/universal_controls.py @@ -3,6 +3,12 @@ from .visibility_control import VISIBILITY_CONTROL from .size_override_controls import SIZE_OVERRIDE_CONTROLS from .spacing_controls import SPACING_CONTROLS from .border_controls import BORDER_CONTROLS -from .hover_controls import HOVER_CONTROLS -UNIVERSAL_CONTROLS = [VISIBILITY_CONTROL] + SIZE_OVERRIDE_CONTROLS + SPACING_CONTROLS + BORDER_CONTROLS + [c_scale()] + HOVER_CONTROLS +# Le panneau "Survol" (HOVER_CONTROLS) a été retiré du panneau de propriétés : +# survoler un élément est conceptuellement un DÉCLENCHEUR (voir la Logique de +# la scène), pas une propriété statique de l'élément — il sera réintroduit +# là-bas. Le mécanisme data-hover-text sous-jacent (voir render_element_html +# et bindHoverTexts() dans play.html) reste fonctionnel pour les éléments qui +# en avaient déjà un, seul le moyen de le RÉGLER depuis ce panneau disparaît +# pour l'instant. +UNIVERSAL_CONTROLS = [VISIBILITY_CONTROL, c_scale()] + SIZE_OVERRIDE_CONTROLS + SPACING_CONTROLS + BORDER_CONTROLS diff --git a/screens/widgets/control_value.py b/screens/widgets/control_value.py index ad9dddc0..d6418b07 100644 --- a/screens/widgets/control_value.py +++ b/screens/widgets/control_value.py @@ -23,6 +23,14 @@ def _control_value(el, control): m = re.search(r"scale\(([\d.]+)\)", transform) return float(m.group(1)) if m else control.get("default", 1) + if ctype == "size": + _kind, name = control["target"].split(":", 1) + raw = style.get(name) + m = re.match(r"^([\d.]+)(px|%)$", str(raw or "")) + if m: + return {"value": float(m.group(1)), "unit": m.group(2)} + return {"value": control.get("default", 0), "unit": "%"} + target = control["target"] if target == "content": return el.get("content") or control.get("default", "") diff --git a/screens/widgets/controls/c_size.py b/screens/widgets/controls/c_size.py new file mode 100644 index 00000000..7adbbb7d --- /dev/null +++ b/screens/widgets/controls/c_size.py @@ -0,0 +1,5 @@ +def c_size(key, label, target, default=0, group=None): + """Taille avec unité choisie par l'utilisateur (% ou px) — voir + size_override_controls.py. 0 = comportement automatique, quelle que soit + l'unité affichée.""" + return {"key": key, "label": label, "type": "size", "target": target, "default": default, "group": group} diff --git a/screens/widgets/default_style_for_widget.py b/screens/widgets/default_style_for_widget.py index 37d1f7a6..89255f31 100644 --- a/screens/widgets/default_style_for_widget.py +++ b/screens/widgets/default_style_for_widget.py @@ -1,14 +1,5 @@ from .widget_meta import widget_meta -# Ces deux réglages utilisent 0 comme valeur sentinelle "automatique" (voir -# size_override_controls.py / _style_string) : contrairement aux autres -# sliders, 0 n'y est PAS une vraie valeur par défaut à figer, donc on ne les -# fige jamais à la création — les autres sliders à 0 par défaut (ex: -# épaisseur de bordure) doivent au contraire être figés explicitement, sinon -# un border-style baké sans border-width associé afficherait une bordure par -# défaut du navigateur (~3px, "medium") au lieu de rester invisible. -_AUTO_SIZE_KEYS = {"child-width", "child-height"} - # Réglages "par côté" (padding/margin/bordure individuels) : contrairement # aux réglages "tous les côtés" équivalents (qui, eux, DOIVENT être figés à # la création — voir plus bas), ceux-ci ne doivent jamais l'être. Une fois @@ -47,6 +38,11 @@ def default_style_and_attributes(widget_key): ctype = control["type"] if ctype in ("checkbox", "toggle_token", "preset", "scale", "color"): continue + if ctype == "size": + # 0 est TOUJOURS la valeur sentinelle "automatique" pour ce + # type de réglage (voir size_override_controls.py) — jamais une + # vraie valeur par défaut à figer à la création. + continue target = control.get("target") if not target or target == "content": continue @@ -54,8 +50,6 @@ def default_style_and_attributes(widget_key): if default in (None, ""): continue if ctype == "slider": - if control["key"] in _AUTO_SIZE_KEYS and str(default) in ("0",): - continue # 0 = comportement automatique, rien à figer if control["key"] in _PER_SIDE_KEYS: continue # réglage par côté : jamais figé (voir commentaire plus haut) unit = control.get("unit", "") diff --git a/static/style.css b/static/style.css index 4d46fd8d..a6b89ccc 100644 --- a/static/style.css +++ b/static/style.css @@ -176,6 +176,10 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d .posGrid{ display:grid; grid-template-columns:1fr 1fr; gap:8px 12px; margin-bottom:14px; } .posGrid label{ font-size:11px; color:var(--text-dim); display:flex; flex-direction:column; gap:4px; } .posGrid input{ background:var(--panel2); border:1px solid var(--border); border-radius:6px; color:var(--text); padding:6px 8px; } +.posGrid input:disabled{ opacity:0.4; cursor:not-allowed; } +.sizeField{ display:flex; gap:4px; } +.sizeField input{ flex:1; min-width:0; } +.sizeField select{ width:auto; flex:0 0 auto; padding:6px 4px; } .controlRow{ margin-bottom:12px; } .controlRow > label{ display:block; font-size:12px; color:var(--text-dim); margin-bottom:4px; } diff --git a/templates/screen_edit.html b/templates/screen_edit.html index 27f3ad79..41d9558d 100644 --- a/templates/screen_edit.html +++ b/templates/screen_edit.html @@ -188,21 +188,49 @@ {% endif %} + {% set size_ctrl_keys = ['child-width', 'child-height'] %} + {% set size_ctrls = {} %} + {% for c in selected_controls if c.key in size_ctrl_keys %}{% set _ = size_ctrls.update({c.key: c}) %}{% endfor %} + +
Position & taille
- - + + + {% if el.parent_id %} + + + {% else %} + {% endif %}
+ {% if el.parent_id %} +

X/Y ne s'appliquent qu'à un élément posé directement sur l'écran — celui-ci suit la disposition de son conteneur. Largeur/Hauteur : laisser vide garde la taille automatique ; "%" (relatif au conteneur) convient à la plupart des cas, "px" pour une taille précise.

+ {% endif %}
- {% set ns = namespace(last_group=None, started=False) %} - {% for c in selected_controls %} + {% for c in selected_controls if c.key not in size_ctrl_keys %} {% if c.group != ns.last_group %} {% if ns.started %} @@ -767,7 +795,11 @@ function initBuilderPanel() { (function () { var xEl = document.getElementById('pos-x'), yEl = document.getElementById('pos-y'); var wEl = document.getElementById('pos-w'), hEl = document.getElementById('pos-h'); - if (!xEl) return; + // pos-w/pos-h n'existent pas pour un élément posé dans un conteneur (sa + // taille se règle via les champs "Largeur/Hauteur (%)"/"fixe (px)" du + // formulaire général à la place, voir size_ctrls plus haut) — X/Y non + // plus, mais ils restent dans le DOM (juste désactivés) pour cet élément. + if (!xEl || !wEl) return; function apply() { var canvasEl = document.getElementById('el-' + CURRENT_SELECTED_ID); var x = parseFloat(xEl.value) || 0, y = parseFloat(yEl.value) || 0; diff --git a/tests/test_confort.py b/tests/test_confort.py index 9216e8df..8105e966 100644 --- a/tests/test_confort.py +++ b/tests/test_confort.py @@ -18,7 +18,13 @@ def _add_element(client, slug, screen_id, widget): # ---------- 3.1 — Interactions au survol ---------- -def test_hover_text_control_saved_and_rendered(client, game): +def test_hover_text_control_removed_from_properties_panel(client, game): + """Le réglage "Survol" a été retiré du panneau de propriétés (voir + universal_controls.py) : survoler un élément est conceptuellement un + déclencheur de la Logique de la scène, pas une propriété statique — + il sera réintroduit là-bas. Poster ctrl_survol_texte ne doit donc plus + avoir d'effet (le mécanisme data-hover-text/bindHoverTexts sous-jacent + reste en place pour un usage futur, voir les tests ci-dessous).""" screen_id = _create_screen(client, game) el_id = _add_element(client, game, screen_id, "titre") client.post(f"/game/{game}/elements/{el_id}/save", data={ @@ -26,7 +32,7 @@ def test_hover_text_control_saved_and_rendered(client, game): }) resp = client.get(f"/game/{game}/play") html = resp.data.decode() - assert 'data-hover-text="mathilde.d@forgebase.fr"' in html + assert 'data-hover-text="' not in html def test_hover_text_absent_by_default_no_regression(client, game): From 4eac20a46457e5a70b8876fdbd821702184a3a50 Mon Sep 17 00:00:00 2001 From: william Date: Sun, 23 Aug 2026 17:16:48 +0200 Subject: [PATCH 03/62] Rewire hover as a flow trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "survol"/"fin_survol" trigger events (mouseenter/mouseleave, same anti-doublon pattern as bindClicks()) so hovering an element can run flow nodes, instead of the old static hover-text-only control removed from the properties panel. Also add "Contenu" as a settable "Modifier un élément" property so an action can display a defined text on another element on hover — the concrete use case that motivated this. Verified live with Playwright: a "Au survol" trigger on one element correctly updates another element's text via the action, and text stays put with no "Fin du survol" wired (explicit, no implicit revert, consistent with "Au clic"). --- screens/flow/constants.py | 9 +++ screens/labels/element_action_properties.py | 8 +++ screens/labels/trigger_labels.py | 2 + templates/play.html | 32 +++++++++++ templates/screen_edit.html | 10 +++- tests/test_confort.py | 64 ++++++++++++++++++++- 6 files changed, 121 insertions(+), 4 deletions(-) diff --git a/screens/flow/constants.py b/screens/flow/constants.py index 84fe0536..c06645d3 100644 --- a/screens/flow/constants.py +++ b/screens/flow/constants.py @@ -13,6 +13,15 @@ TRIGGER_EVENTS = [ ("clic", "Au clic"), ("soumission", "À la soumission"), + # 3.1 (Confort) — interactions au survol, reconstruit comme déclencheur + # de flow (au lieu d'un réglage statique dans le panneau de propriétés, + # voir universal_controls.py) : "survol" et "fin_survol" sont deux + # déclencheurs distincts et explicites (comme "Au clic"), sans effet + # implicite — un créateur qui veut qu'un texte affiché au survol + # disparaisse ensuite doit poser l'action inverse sur "Fin du survol" + # lui-même, plutôt que de compter sur un retour automatique. + ("survol", "Au survol"), + ("fin_survol", "Fin du survol"), # Pas de "trigger_element_id" pour celui-ci : il concerne l'ÉCRAN entier, # pas un élément précis (voir 1.2 dans claude/forge-engine-lacunes-boitemail.md # côté projet Forge — "affichage piloté par la donnée"). Se déclenche diff --git a/screens/labels/element_action_properties.py b/screens/labels/element_action_properties.py index 0a479aa3..84da90b2 100644 --- a/screens/labels/element_action_properties.py +++ b/screens/labels/element_action_properties.py @@ -2,6 +2,14 @@ ELEMENT_ACTION_PROPERTIES = [ ("visibilite", "Visibilité"), + # Voir TRIGGER_EVENTS ("survol"/"fin_survol") : c'est ce qui permet à un + # créateur de reconstruire l'ancien comportement "texte affiché au + # survol" (et bien d'autres) à la main dans la Logique de la scène — + # poser "Modifier un élément → Contenu" sur un déclencheur "Au survol" + # d'un AUTRE élément. La valeur est un texte littéral (pas encore de + # binding vers un champ d'objet ici — voir applyOpenRowBindings pour le + # mécanisme équivalent, propre à "Ouvrir la ligne cliquée"). + ("contenu", "Contenu — texte affiché"), ("bg_color", "Couleur de fond"), ("border_color", "Couleur de bordure"), ("width", "Largeur en %"), diff --git a/screens/labels/trigger_labels.py b/screens/labels/trigger_labels.py index c72a0e5d..49bc3531 100644 --- a/screens/labels/trigger_labels.py +++ b/screens/labels/trigger_labels.py @@ -2,4 +2,6 @@ TRIGGER_LABELS = { "clic": "Au clic", "soumission": "À la soumission", "affichage": "À l'affichage de l'écran", + "survol": "Au survol", + "fin_survol": "Fin du survol", } diff --git a/templates/play.html b/templates/play.html index 02c7ff76..1dc8e9c2 100644 --- a/templates/play.html +++ b/templates/play.html @@ -270,6 +270,26 @@ }); } + // Déclencheurs "Au survol" / "Fin du survol" — même principe que + // bindClicks() (garde-fou anti-doublon compris), mais sur mouseenter/ + // mouseleave plutôt que click, et les deux événements cherchés + // séparément puisque ce sont deux déclencheurs distincts et indépendants + // (voir le commentaire sur TRIGGER_EVENTS dans screens/flow/constants.py + // — pas de retour automatique à l'état d'avant survol, un créateur qui en + // veut un doit poser l'action inverse sur "Fin du survol" lui-même). + function bindHoverTriggers() { + document.querySelectorAll('[data-element-id]').forEach(elDiv => { + if (elDiv.dataset.hoverTriggerBound) return; + const elementId = parseInt(elDiv.dataset.elementId, 10); + const enterTrigger = findTriggerNode(elementId, 'survol'); + const leaveTrigger = findTriggerNode(elementId, 'fin_survol'); + if (!enterTrigger && !leaveTrigger) return; + elDiv.dataset.hoverTriggerBound = '1'; + if (enterTrigger) elDiv.addEventListener('mouseenter', () => runFlowFrom(enterTrigger.id)); + if (leaveTrigger) elDiv.addEventListener('mouseleave', () => runFlowFrom(leaveTrigger.id)); + }); + } + // Résout, sur l'écran couramment affiché, les placeholders "champ entre // doubles accolades" restés tels quels dans le HTML statique (hors // Répéteur, donc jamais résolus au moment du rendu serveur) avec les @@ -340,6 +360,7 @@ applyOpenRowBindings(); bindClicks(); bindHoverTexts(); + bindHoverTriggers(); // Réévalue aussi les nœuds "À l'affichage de l'écran" de l'écran où // l'on se trouve déjà : une donnée vient de changer (ex: un outil // qu'on vient de débloquer), donc l'affichage doit se mettre à jour @@ -358,6 +379,16 @@ }; function applyElementProperty(targetEl, property, value) { + if (property === 'contenu') { + // Voir ELEMENT_ACTION_PROPERTIES ("contenu") : texte littéral, pas de + // binding vers un champ d'objet ici (contrairement à + // applyOpenRowBindings, propre à "Ouvrir la ligne cliquée"). Comme + // pour les autres réglages ci-dessous, cible n'importe quel élément — + // pointer ceci vers un conteneur remplacerait tout son contenu + // imbriqué, à éviter. + targetEl.textContent = value == null ? '' : value; + return; + } if (property === 'surbrillance') { // 3.3 (Confort) — surbrillance générique dynamique : un liseré qui // clignote (voir la classe .forgeHighlight et son animation dans le @@ -603,6 +634,7 @@ // directement sur l'écran. bindClicks(); bindHoverTexts(); + bindHoverTriggers(); diff --git a/templates/screen_edit.html b/templates/screen_edit.html index 41d9558d..3b518adb 100644 --- a/templates/screen_edit.html +++ b/templates/screen_edit.html @@ -420,6 +420,7 @@ +