Commit Graph
17 Commits
Author SHA1 Message Date
williamandClaude Sonnet 5 8cffbeac68 Donnée liée : conditions ET/OU illimitées + conditions de logique sur une variable globale
Deux fonctionnalités demandées, développées et corrigées dans cet
échange :

1. "Donnée liée" (Texte/Titre) : le réglage à 2 filtres fixes (toujours
   combinés en ET) devient une liste de conditions ILLIMITÉE, avec un
   choix ET/OU pour les combiner (screens/widgets/controls/c_clause_list.py,
   screens/clause_list_codec.py). Rétrocompatible avec les anciens
   éléments (_data_filtre_champ/_data_filtre2_champ), convertis à la
   volée à la lecture, sans migration. Après un premier essai à la
   présentation trop compacte et technique (retour utilisateur : "pas de
   champ technique, pas de notation bizarre {{ }}"), la présentation
   finale reprend EXACTEMENT l'ancien style (labels "Champ"/"...est"/
   "...cette valeur", même sélecteur de valeur fixe/dynamique/variable
   déjà existant, jamais la syntaxe brute), simplement répétée par
   condition (templates/partials/clause_row.html), avec un bouton
   "+ Ajouter une condition" bien visible et une liste scrollable
   (static/style.css, .clauseListWrap). Le même moteur (filter_repeater_
   rows.py généralisé) profite aussi au Répéteur de données en interne.

2. Nœud Condition de la Logique de la scène : peut désormais tester une
   VARIABLE GLOBALE en plus d'un champ d'objet (cond_source/cond_variable/
   cond_variable_chemin — screens/flow/ensure_flow_schema.py), sur la
   clause principale ET chaque clause supplémentaire (ET/OU). Évalué côté
   CLIENT (templates/play.html, evaluateConditionClause), contre un
   nouveau gameData.variables exposé par full_game_payload.py — tenu à
   jour par refreshRuntimeData() après toute action qui modifie une
   variable, sans changement supplémentaire nécessaire. Le panneau de
   condition reste utilisable même sans aucun objet défini dans le jeu
   (avant, il disparaissait entièrement).

Vérifié : 153 tests pytest (nouveaux : test_data_binding_clause_list.py,
test_condition_variable.py) + logique JS d'évaluation des conditions
vérifiée isolément avec Node (variable scalaire, objet avec chemin
chaîné, tableau par index, variable introuvable, booléen, rétrocompatibilité
legacy) + rendu des deux pages (éditeur/jeu) vérifié sur le vrai projet
"test" en plus des jeux de test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 09:05:02 +02:00
williamandClaude Sonnet 5 7c237d6f1c Retire le voile plein écran et le forçage de visibilité de l'éditeur (retour utilisateur)
Deux retours après le dernier correctif (78a373a, qui forçait
"display:flex" dans l'éditeur pour que la boîte de dialogue reste
visible) :
- "je souhaite avoir la main sur la visibilité de la modale sinon elle
  s'affiche toujours sur la scène, court-circuite ma logique" — le
  forçage empêchait de vraiment utiliser "Visibilité" pendant l'édition.
- "quand j'édite la modale ou quand je la mets dans une scène je
  souhaite que rien ne soit assombri, l'assombrissement ne se fait que
  quand la scène est jouée" — le voile plein écran (position:fixed +
  fond assombri) restait aussi actif dans l'éditeur.

Correctif (render_overlay.py) : le voile plein écran ET le forçage de
visibilité sont retirés de l'ÉDITEUR — ce widget s'y comporte maintenant
comme un CONTENEUR NORMAL (position/taille selon x/y/width/height, aucun
voile, réglage "Visibilité" respecté normalement, comme n'importe quel
autre widget masqué). Le comportement plein écran/voile/masquage par
défaut n'est conservé qu'en mode JOUABLE (ctx["_forge_play_mode"]).

En creusant pourquoi "Visible" ne suffisait pas à faire réapparaître la
boîte dans l'éditeur (menant l'utilisateur à essayer "Invisible" à la
place, visible dans ses captures), trouvé un vrai bug latent dans
save_element_controls.py : l'option "Visible" du réglage "Visibilité" ne
touche volontairement jamais "display" (pour ne pas écraser le
"display:flex" d'un conteneur en disposition ligne/colonne — voir
visibility_control.py) — ça fonctionne seulement parce que, pour un
widget AVEC un réglage "Disposition interne", celui-ci réaffirme lui-même
un display non-"none" au même enregistrement. La "superposition" n'a PAS
ce réglage : "display:none" (posé à la création ou par un "Masqué"
précédent) restait donc bloqué pour toujours, quel que soit le nombre de
fois où "Visible" était ensuite choisi. Corrigé : "Visible" efface aussi
"display" pour tout widget SANS réglage "Disposition interne" (safe : les
widgets qui EN ont un ne sont pas concernés, donc aucune régression sur
leur comportement existant).

Tests mis à jour (l'ancien test attendait le forçage, désormais retiré) +
nouveau test qui couvre le cycle complet (masqué par défaut -> "Visible"
choisi -> apparaît sans voile dans l'éditeur -> voile plein écran
retrouvé en mode jouable). 140 tests au vert au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 07:14:51 +02:00
williamandClaude Sonnet 5 6e46949cf8 Corrige la boîte de dialogue posée comme élément de jeu réutilisable : conteneur vide au lieu du dialogue
Bug rapporté : poser un élément de jeu ("Mes éléments de jeu") dont le
modèle n'est qu'une "Superposition / boîte de dialogue" sur une vraie
scène affichait un conteneur vide à l'endroit du dépôt — jamais le
dialogue. La boîte de dialogue existait bien (masquée comme prévu, en
attente d'une action qui l'affiche) : c'est l'enveloppe qui l'entoure qui
n'aurait jamais dû être visible.

Cause : tout exemplaire d'élément de jeu est posé avec le widget générique
"conteneur" par défaut (add_element.py, colonne default_widget) — son
contenu réel (le modèle) est rechargé EN DIRECT à l'intérieur
(_render_element_type_children), mais l'enveloppe "conteneur" elle-même
reste une boîte NORMALE, toujours visible, avec sa propre couleur de fond/
bordure et sa position fixe sur le canevas (contrairement à une
superposition posée directement, qui, elle, démarre masquée). Résultat :
une boîte vide et permanente à l'endroit du dépôt, pendant que le vrai
dialogue (démarré masqué, correctement) reste invisible en dessous/
au-dessus tant qu'aucune action ne le déclenche.

Correctif : quand le modèle ENTIER d'un élément de jeu n'est qu'une seule
superposition (screens/element_types/is_overlay_only.py, nouveau), on
court-circuite entièrement l'enveloppe "conteneur" (render_element_html.py)
et on retire aussi le z-index de son cadre de positionnement
(element_style_filter.py, list_elements.py) — même raison que pour une
superposition posée directement (81c31a9) : sans ça, ce cadre reste un
candidat à piéger le z-index:9999 du dialogue rendu à l'intérieur dès
qu'un autre élément de la scène a un z-index plus grand.

Nouveau test, confirmé en échec sur l'ancien code (même "class=\"box\""
fantôme reproduit) puis au vert avec le correctif. 140 tests au vert au
total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 06:58:44 +02:00
williamandClaude Sonnet 5 19d3810164 Calcule rendered_html pour CHAQUE élément, pas seulement le premier niveau
Cause racine réelle des 2 précédents correctifs (commits 20adfa4,
f4a7a73, 5e4e226, f016a81) qui n'avaient AUCUN effet visible malgré des
redémarrages en règle : list_elements() ne calculait "rendered_html" que
pour les éléments de PREMIER NIVEAU (parent_id NULL). Le nouveau ciblage
côté JS (play.html) cherche pourtant à repérer, pour un élément imbriqué
comme le Répéteur ou "mail content", s'il porte lui-même un marqueur ou
un {{champ}} non résolu — mais côté serveur, ces éléments n'avaient tout
simplement PAS de rendered_html du tout : `e.rendered_html` valait
`undefined`, donc `hasMarker`/`hasUnresolvedPlaceholder` retombaient
toujours à `false` pour eux, laissant SEUL le conteneur racine de
premier niveau comme candidat — reproduisant exactement le bug d'origine
(tout l'écran régénéré à chaque clic) qu'aucun des correctifs côté JS ne
pouvait donc jamais résoudre, quelle que soit la justesse de leur
logique de filtrage.

Fix : chaque élément (imbriqué ou non) reçoit désormais son propre
rendered_html — un élément imbriqué s'y retrouve deux fois (une fois
dans le rendered_html de son ancêtre de premier niveau, utilisé pour le
rendu HTML initial de la page ; une fois dans le sien propre, utilisé
par le ciblage précis côté JS). Vérifié par simulation directe sur le
payload réel du jeu de test : les cibles calculées sont maintenant
exactement les 3 jauges, le Répéteur et "mail content" — plus jamais le
conteneur racine de l'écran.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 06:49:41 +02:00
williamandClaude Sonnet 5 6894c5fc95 Remplace le confirm() natif par une modale custom qui avertit des suppressions en cascade
Depuis le dernier correctif, supprimer un élément supprime aussi en
silence tout nœud de la Logique de la scène qui le référence (déclencheur/
action, voir delete_element.py) - nécessaire pour éviter le plantage
"FOREIGN KEY constraint failed", mais l'utilisateur n'était jamais prévenu
qu'un bout de sa logique disparaissait en même temps.

Nouvelle route GET .../elements/<id>/delete-impact (element_delete_
impact.py) : calcule, sans rien supprimer, combien de nœuds de la Logique
de la scène référencent cet élément OU l'un de ses descendants (partage
element_descendant_ids.py avec delete_element.py, pour rester exactement
cohérent avec ce qui sera réellement supprimé).

Les deux boutons "supprimer" de screen_edit.html (élément sélectionné, et
onglet d'un widget Onglets) ouvrent maintenant une modale custom
(deleteConfirmModal, même famille que le sélecteur d'icônes) au lieu du
confirm() natif du navigateur : elle interroge cette route juste après
ouverture et affiche un avertissement dédié si le nombre remonté est non
nul, avant que l'utilisateur ne confirme quoi que ce soit - impossible à
faire avec confirm(), dont le texte est figé au moment du rendu de la
page. La confirmation soumet ensuite le formulaire normalement (via
requestSubmit(), intercepté par pjax.js comme n'importe quel autre
formulaire).

Ajoute deux tests pour la nouvelle route (impact nul, impact non nul sans
rien supprimer).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:27:34 +02:00
williamandClaude Sonnet 5 1e86077e45 Corrige un plantage à la suppression d'un élément référencé par la Logique de la scène
Bug remonté : supprimer un élément plantait avec sqlite3.IntegrityError:
FOREIGN KEY constraint failed.

Cause : trigger_element_id/target_element_id (_flow_nodes, voir
ensure_flow_schema.py - un nœud "clic sur cet élément" ou "Modifier cet
élément"/"Activer cet onglet") et target_element_id (l'ancien système
_actions, conservé pour compatibilité) référencent _screen_elements(id)
SANS ON DELETE CASCADE - volontairement, un élément ne doit pas pouvoir
disparaître "par erreur" en cascade depuis un nœud de logique qu'on
modifie ailleurs. Mais ça veut dire que delete_element.py, qui ne
supprimait jusqu'ici que la ligne elle-même, faisait échouer PRAGMA
foreign_keys=ON (db/connection.py) dès qu'un nœud de logique existant
référençait encore l'élément.

Fix : delete_element.py nettoie maintenant ces références AVANT de
supprimer l'élément - pas seulement pour l'élément explicitement supprimé,
mais pour tous ses DESCENDANTS aussi (leur suppression est cascadée
automatiquement au niveau SQL via parent_id, sans repasser par ce
fichier, donc sans ce nettoyage si on ne le fait pas explicitement).

Ajoute tests/test_delete_element_referenced_by_flow.py (élément
référencé comme déclencheur, comme cible d'action, et cas d'un conteneur
supprimé dont un descendant est référencé).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:19:52 +02:00
williamandClaude Sonnet 5 4dc42a7f3b Corrige les réglages fantômes d'un exemplaire d'élément de jeu déjà posé
Bug remonté : la taille d'un Titre réglée à 18px dans le modèle affichait
21px "sur l'écran". Cause : un exemplaire posé AVANT le passage en mode
"toujours lié au modèle" (tour précédent) avait été créé par l'ancien
mécanisme instantiate_template_tree (retiré), qui copiait tout l'arbre en
base - ces lignes copiées (l'ancien enfant "Titre", encore à 21px depuis
avant le changement) ne sont plus jamais RENDUES (le contenu vient
désormais toujours en direct du modèle, voir le tour précédent), mais
restaient toujours sélectionnables dans l'arborescence de l'éditeur, avec
leurs propres réglages jamais synchronisés. Cliquer dessus dans l'arbre
affichait donc ses vieux réglages (21px) dans le panneau de propriétés,
donnant l'impression trompeuse que le modèle (18px) n'était pas pris en
compte - alors que le rendu réel utilisait déjà correctement 18px.

Fix, dans list_elements.py : tout élément dont un ANCÊTRE a
element_type_id réglé est maintenant exclu de la liste renvoyée à
l'éditeur (arborescence, sélection, panneau de propriétés) - son contenu
n'a plus aucune existence propre, seul l'écran-modèle fait foi. Les
lignes elles-mêmes restent en base (pas de suppression, un simple filtre
en lecture) mais ne sont plus jamais atteignables depuis l'éditeur.

Ajoute un test de régression qui simule exactement ce scénario (ligne
orpheline avec un ancien réglage figé) et vérifie qu'elle n'apparaît plus
nulle part - ni dans le canevas, ni via une sélection directe par id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:10:30 +02:00
williamandClaude Sonnet 5 e9a991ed13 Un exemplaire d'élément de jeu posé sur un écran reste maintenant lié à son modèle
Jusqu'ici, poser un élément de jeu depuis le catalogue ("Mes éléments de
jeu") copiait tout son arbre en base (instantiate_template_tree) : chaque
exemplaire devenait indépendant, y compris de son propre modèle - modifier
l'élément de jeu dans son éditeur n'avait plus aucun effet sur les
exemplaires déjà posés ailleurs.

Change ce comportement pour qu'un exemplaire reste TOUJOURS lié à son
modèle, sur le même principe déjà utilisé par un modèle de ligne de
Répéteur (jamais copié, rechargé en direct à chaque affichage - voir
_load_template_tree/_render_repeater) : add_element.py ne crée plus
qu'UNE SEULE ligne plate (avec sa position/taille propres à cet
exemplaire) au lieu de copier tout l'arbre, et render_element_html.py
recharge le contenu depuis l'écran-modèle à chaque rendu quand
element_type_id est réglé. Modifier l'élément de jeu dans son propre
éditeur met donc à jour tous ses exemplaires déjà posés, sur n'importe
quel écran (y compris ceux placés AVANT ce correctif, qui portaient déjà
element_type_id sur leur ligne de premier niveau), sans avoir à les
retoucher un par un.

Contrepartie assumée (discutée avec l'utilisateur avant ce changement) :
un exemplaire ne peut plus être personnalisé individuellement à
l'INTÉRIEUR (texte, couleur d'un enfant précis...) - seules sa position et
sa taille sur l'écran restent propres à chaque exemplaire. Pour changer le
contenu, il faut désormais passer par l'éditeur de l'élément de jeu
lui-même.

instantiate_template_tree.py, devenu inutilisé, est supprimé.

Ajoute tests/test_element_type_live_instances.py (mise à jour d'un
exemplaire déjà posé, propagation jusqu'à "Jouer", position toujours
indépendante par exemplaire) et met à jour un commentaire de test devenu
obsolète dans test_screens_and_elements.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 15:26:45 +02:00
williamandClaude Sonnet 5 bda082bffd Ajoute une propriété "Ombre portée" (box-shadow), disponible sur tout widget
Nouveau groupe "Ombre" dans le panneau de propriétés, à côté de "Bordure"
(même universalité — voir UNIVERSAL_CONTROLS) : décalage X/Y, flou,
étendue, couleur et opacité, combinés en une seule valeur CSS box-shadow
(couleur+opacité fusionnées en rgba(), un <input type="color"> seul ne
portant pas de canal alpha). Décalages/flou/étendue tous à 0 = pas
d'ombre, même convention que border-width à 0 = pas de bordure.

Nouveau ctype "shadow" (c_shadow.py), suit exactement le même principe
que le ctype "size" déjà existant (size_override_controls.py) : plusieurs
entrées de formulaire pour un seul réglage, composées/décomposées dans
save_element_controls.py et control_value.py plutôt que passées par le
chemin générique clé->valeur.

Ajoute tests/test_shadow_controls.py (présence dans le panneau,
composition de la valeur CSS, aller-retour dans le formulaire, remise à
zéro qui retire l'ombre).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 14:37:38 +02:00
williamandClaude Sonnet 5 c04bc0b926 Ajoute la condition de visibilité et les variables globales
Nouveau panneau "Condition de visibilité" disponible dans les propriétés
de TOUT élément (widget) : permet de masquer un élément en mode jouable
selon deux moyens, au choix -
  - une variable globale (nom + type + valeur, une seule par jeu, stockée
    dans une nouvelle table _global_variables) ;
  - le champ d'un objet de données existant (même convention "état de
    partie" - une seule ligne - déjà utilisée par la Jauge).

Une variable ne servant à rien si elle ne peut jamais changer en cours de
partie, ajoute aussi une nouvelle action de flow "Modifier une variable
globale" (parallèle à "Modifier une donnée"), avec sa propre route
d'exécution serveur et son sous-formulaire dans l'éditeur de logique de
scène. Une variable peut aussi se créer à la volée depuis le sélecteur du
panneau de visibilité, sans quitter les propriétés de l'élément.

La condition n'est évaluée qu'en mode jouable (/game/<slug>/play), jamais
dans l'éditeur, pour que l'élément reste toujours sélectionnable. Un
élément masqué se réévalue en direct après toute action "Modifier une
donnée/variable", via le même mécanisme de rafraîchissement déjà utilisé
par la Jauge et le Répéteur.

Corrige au passage deux bugs découverts en testant bout en bout : (1)
apply_ctx plantait sur le nouveau marqueur interne _forge_play_mode (un
booléen parmi les {{champ}} à substituer, qui attend des chaînes) ; (2)
_compare traitait toute valeur booléenne stockée en chaîne ("0" inclus,
donc toujours vraie en Python) comme vraie - correct pour les champs
d'objet (entiers SQLite) mais faux pour les variables globales (toujours
stockées en texte).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 09:34:10 +02:00
william 33a887e7ba Move icon gallery to the left panel, drop the redundant Icône tile, add drag-into-container in the tree
- The "icone" widget no longer appears as a generic tile in "Ajouter un élément sur l'écran" / "Ajouter DANS ce conteneur" — the icon gallery (now living under "Ajouter un élément sur l'écran" in the left panel instead of the right one) is the only way to add one, always pre-set to the icon you picked.
- The element tree now supports dragging an element onto a container row to move it inside (last child), from anywhere in the tree — not just reordering within the same parent. Hovering a container row splits it into before/after/inside zones (thirds) when dragging a sibling, or "inside only" when dragging from elsewhere in the tree. New move_element_to_container() rejects non-container targets and cycles (dropping a container into itself or one of its own descendants) silently, mirroring reorder_element's existing safety pattern.

Verified end to end: gallery renders in the left panel with no icone tile in the widget grids, and the move endpoint correctly reparents, rejects a cycle, and rejects a non-container target. Full suite green (89).
2026-08-25 06:03:02 +02:00
william 38f33d88d9 Make Bulma the default style engine for Bouton/Titre/Conteneur, layered under existing inline customization
Adds a new "class:" control-target kind (save_element_controls.py, _visible_attrs.py) alongside the existing content/attr/style ones, so a widget can carry CSS classes built from independent named slots (color, size, shape...) without them overwriting each other. Bouton and Titre get their Bulma base class (button/title) via fixed_attrs; Conteneur gets an opt-in "Carte (Bulma)" preset instead of a forced default, since it's also used as an invisible layout wrapper. Bouton's font-size/border-radius sliders no longer freeze their default value into inline style at creation (new c_slider no_freeze flag), so Bulma's own button styling shows through until a user actually customizes it — inline style still wins over any class the moment it's set, exactly like today.

Loads bulma.min.css via CDN in screen_edit.html and play.html, same pattern as animate.css.
2026-08-24 12:54:50 +02:00
william 4b05301e2e Add drag-and-drop reordering of sibling elements in the tree panel
Elements can now be reordered within the same container by dragging a row above or below another in the left-hand element tree.
2026-08-24 08:43:36 +02:00
williamandClaude Sonnet 5 17fa4cf087 Add an Onglets (Tabs) widget
New widget where each tab is a real "conteneur" element posed as a child
(see screens/elements/add_tab.py) — this reuses everything that already
exists for a normal container (adding a Répéteur/Conteneur/etc. inside via
"Ajouter DANS ce conteneur", renaming to change the tab's visible label,
deleting via the standard trash icon) instead of inventing a separate
storage format for tabs.

The widget's own properties panel gets a dedicated "Onglets" section to
add a tab, rename one, jump to its content, or delete it. Rendering
(render_onglets.py) builds a tab bar + one panel per tab, switched
client-side (forgeShowTab, in both screen_edit.html and play.html) with
only one panel visible at a time.

Distinct from the existing "activer_onglet" flow action (2.3, manual
show-one/hide-siblings) — that stays available for custom show/hide
wiring; this widget is the turnkey version with tab management built in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 21:42:39 +02:00
williamandClaude Sonnet 5 bb79f2f93d Redesign the screen editor's element tree and add duplication
The "Éléments de cet écran" tree now renders every level of nesting
(previously stopped after one level of children) as a compact single-line
list, and right-clicking a row opens a context menu to duplicate the
element (and its full subtree) in place, in its current container.

Also: all property panels start collapsed instead of some being open by
default, the redundant nested element list inside "Ajouter DANS ce
conteneur" is removed (it only needs the widget picker), and the
now-unneeded "a container is selected" warning banner is gone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:53:38 +02:00
william 4a593d07e4 Consolidate the properties panel: Position & taille, no more Taille/Survol/duplicate Disposition
- 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.
2026-08-23 12:14:00 +02:00
williamandwilliam 3f4ebc4527 first commit
Build and deploy / deploy (push) Successful in 10s
Build and deploy / build-and-push (push) Successful in 17s
2026-08-21 16:23:49 +02:00