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>
data_definition_options() used to exclude relation-type fields from its field list entirely, so a filter/binding could never reference "the linked object" of a row — and even when a relation field's clean name was typed manually (as the earlier "level.parcour" example needed), it silently matched nothing: every column lookup for a filter/repeater field used the field's own name, but a relation is actually stored in a "<field>_id" column (see create_definition.py), so the lookup always missed.
Relation fields now appear in the champ dropdowns (Répéteur's filtre_champ/filtre2_champ, Donnée liée's data_filtre_champ/data_filtre2_champ) labeled with the object they point to (e.g. "parcour (→ parcours)"), and a new _field_column() helper in filter_repeater_rows.py resolves the right "<field>_id" column whenever the field turns out to be a relation — used consistently by the filter comparison itself, the "{{Objet.champ}}" dynamic-value resolver, the repeater's row content ({{champ}}), and the Donnée liée row context. Jauge's own champ/champ_nom pickers (which need an actual displayable value, not an id) still exclude relations, both server- and client-side.
Verified end to end: the dropdown shows the relation field with its target-object label, and filtering "level" rows by the clean relation field name "parcour" (not "parcour_id") against a dynamic {{game.current_parcours}} reference now actually matches, alongside the existing "number" filter. Full suite green (89).
Two settings previously required typing an exact field or object name by hand:
- "champ" (Répéteur's filtre_champ/filtre2_champ, Donnée liée's data_filtre_champ/data_filtre2_champ) is now a dropdown of the actual fields on the object already chosen for that widget — pre-populated server-side (controls_with_values.py) and live-updated client-side without a reload when you change the object (bindDefinitionFieldSelects(), generalizing the existing Jauge champ/row_id pattern).
- "valeur" (the comparison value, which could already reference another object's field via a "{{Objet.champ}}" string typed by hand) is now a "Valeur fixe" / "Donnée d'un autre objet" toggle — the dynamic mode shows an object dropdown and a dependent field dropdown, and picking from them reconstructs that internal reference string automatically (initFilterValuePickers()/updateFilterValueFromDynamic()). The underlying stored value and _resolve_filter_value's parsing are unchanged, so existing elements using the old typed syntax still load correctly and populate the pickers on open.
Guards against writing a literal "{{...}}" pair directly in the Jinja template source (caught by a real render error while building this — Jinja parses {{/}} anywhere in the file, including inside comments) by building that syntax from split string literals in JS instead.
Verified end to end: repeater panel renders the pickers, saving with a dynamic reference filters correctly against live data (current_level changing which row shows), and reloading pre-selects the right object/field in the dropdowns. Full suite green (89).
- 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).
Self-hosting Font Awesome's font files (previous commit) didn't fix it either: every icon in the picker still failed identically. That rules out CDN/network blocking specifically and points to something in the browser blocking custom webfont loading altogether regardless of origin (common with strict anti-fingerprinting protection, e.g. Brave's font blocking or certain privacy extensions — @font-face loading is a well-known fingerprinting vector).
Replaced the whole mechanism: each of the 258 curated icons is now a real downloaded SVG file (static/icons/*.svg, sourced from Font Awesome 5 Free's official SVG package) displayed via CSS mask-image (.icon-svg in style.css) instead of a font glyph. A masked SVG isn't a font resource at all, so it isn't subject to webfont-blocking — and it still colors via the existing "color" style property (background-color:currentColor) and sizes via font-size (1em), so no change to how the widget's other controls work.
The "Icône" widget now stores just the icon's slug (e.g. "trophy") in a dedicated attribute instead of a full CSS class string, rendered by a new special_render (render_icone.py). Updated the add-gallery and the properties-panel picker modal to use the same mask technique for their previews, and element_add.py to validate the slug against the curated list. Removed the now-unused self-hosted Font Awesome CSS/webfont files and the <link> tags — no font dependency left for icons at all.
Verified end to end (gallery renders with real SVG previews, simulated add creates a correctly-attributed element, the SVG file is actually served, the per-element picker reflects the current icon). Full suite green (89).
New "Icône" widget (<i class="...">, color/size customizable exactly like any other widget) plus a curated set of ~250 verified Font Awesome 5 Free solid icon names (fontawesome_icons.py) — not the full ~1500-icon catalog, since an embedded list needs to be guaranteed accurate (a wrong class name silently renders as a blank glyph); any other valid FA5 class still works by typing it directly into the widget's "Icône" setting.
The gallery lives in the (now always-visible) top of the right floating panel, searchable, with each tile both clickable and HTML5-draggable onto the canvas — either action posts to element_add with the chosen icon_class, which now seeds the new element's class instead of leaving it on the generic default. Available in the element-type template editor too, since it reuses screen_edit.html.
Loads Font Awesome 5.15.4 (cdnjs) alongside the existing animate.css/Bulma links, in both the editor and /play.
Verified end to end: gallery renders with real icon glyphs, clicking/simulated-drop creates a correctly-classed <i> element, and the actual /play route renders it with Font Awesome loaded. Full suite green (89).
Adds a shared bulma_variants.py (color/size option lists) and expands BULMA_*_CONTROLS in bulma_controls.py to cover each component's full modifier set: Bouton (colour incl. white/light/dark/black/text, light-shade toggle, size, rounded, outlined, inverted, static, loading, fullwidth), Titre (Bulma size is-1..is-6 independent of heading level, is-spaced), Jauge (size), Tableau (bordered/fullwidth/striped/hoverable/narrow, now toggleable instead of hardcoded), Onglets (alignment, boxed/toggle/toggle-rounded style, size, fullwidth), and every form field — champ_texte/email/mot_de_passe, zone_texte, liste_deroulante (colour, size, rounded, static).
render_select.py/render_onglets.py/render_jauge.py now redirect the merged "class" from _visible_attrs to the actual Bulma sub-element (the .select wrapper, the .tabs div, the <progress> tag) instead of the outer positioning wrapper, since that's what needs to carry the modifier classes.
Also fixed a real latent bug found while wiring this up: a checkbox control with default=True (e.g. Tableau's "Première ligne = en-tête") was never actually applied on a freshly created element — default_style_and_attributes unconditionally skipped ALL checkbox types at creation, so the panel showed it checked while the element itself had nothing set. Now a checkbox's default=True is frozen at creation like any other meaningful default; default=False (the common case) is unaffected.
Verified end to end: every widget's variant controls save and render the right class tokens (spot-checked titre/tableau/onglets/champ_texte/liste_deroulante/jauge), full suite green (89 passed).
- Jauge now renders a real <progress class="progress"> instead of a hand-built pair of absolutely-positioned divs. The bas/haut color interpolation still works, set via Bulma's own --bulma-progress-value-background-color CSS custom property rather than fighting the class.
- Onglets' tab strip is now genuine Bulma tabs markup (tabs > ul > li, is-active on the li) instead of custom forgeTabBar/forgeTabBtn classes; forgeShowTab (duplicated in screen_edit.html and play.html) now toggles is-active to match.
- Champ texte/email/mot de passe get class="input", Zone de texte gets class="textarea", Case à cocher/Bouton radio's existing <label> wrapper gets class="checkbox"/"radio" (Bulma's own convention — the structure already matched, just needed the class), Liste déroulante is wrapped in Bulma's required <div class="select"> (a bare class on the <select> itself has zero effect in Bulma), Tableau gets class="table is-bordered is-fullwidth" with the per-cell inline borders removed so Bulma's own table styling applies.
- Removed the now-dead .forgeTabBar/.forgeTabBtn CSS.
Updated tests/test_jauge.py and tests/test_onglets_widget.py assertions to match the new markup (value="X" attribute instead of width:X% inline style, --bulma-progress-value-background-color instead of background-color, forgeShowTab(this) marker instead of the removed forgeTabBtn class) — same behavior, different rendering mechanism. Full suite green (89 passed), verified end to end against the real "test" project via the actual /play route.
Every widget gets padding/margin/border-width reset to 0 inline at creation (needed for a free-positioning canvas, otherwise every <h1>/<ul>/<p> would carry the browser's default spacing) — but for a widget that carries an external class like Bulma's button/box (see fixed_attrs in registry.py), inline style always wins over the class, so this reset silently stripped Bulma's own padding and border, making them look nothing like the real components. Bouton/Conteneur/Titre now skip this reset for those three properties, letting Bulma's own box model show through until a value is actually customized.
Also cleaned the frozen 0px values out of existing elements in the two live project databases (projects/test, projects/test-2) — only removed values that exactly matched the old frozen default, any genuinely customized non-zero padding/margin/border-width was left untouched.
Dropped the "Style Bulma" select that required manually choosing "Carte" to get Bulma's box class — the widget now always carries class="box", same unconditional pattern as Bouton (button) and Titre (title). A custom background color still overrides it the moment it's set, same as before.
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.
Adds a "Donnée liée" settings group to Texte/Titre: pick an object, optionally match it to the current game state via 1-2 filter conditions (same engine as the data-repeater's filter, including {{Objet.champ}} cross-references), and use {{champ}} in the text content to show a field from that one matching row. Unlike the data repeater — built for showing a list of rows — this covers displaying a single computed value (e.g. the objective of the level matching the game's current parcours/level) without wrapping it in a repeater.
Needed to filter a "level" object by both its parcour_id and its number at once (e.g. show the level matching the game's current_parcours AND current_level) — the previous filter only supported a single condition.
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>
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>
Center the name+bar block vertically within the Jauge's own box
(justify-content:center) so it no longer looks pinned to the top once a
name label adds extra content height.
Add appearance settings for the name: position (above the bar, or beside
it on the left), alignment when above (centered or left-aligned), font
family, font size, bold, and italic.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A Jauge posée dans un conteneur a une hauteur "auto" (voir
_style_string) — "flex:1 1 auto" seul n'avait alors rien à répartir
(le conteneur flex lui-même n'a pas de hauteur définie), donc le
wrapper de la barre s'effondrait à 0px : seul le nom restait visible,
la jauge elle-même disparaissait. Ajout d'un "min-height" plancher
sur ce wrapper, qui laisse toujours la barre visible dans ce cas tout
en la laissant grandir avec flex:1 si l'élément a une vraie hauteur.
A Jauge could only target a whole object, not a specific record —
three gauges pointing at the same "value" field (e.g. Réputation/
Trésorerie/Confiance in one "jauge" object) all silently showed the
most recent row's value, with no way to tell them apart.
Add "Enregistrement (ligne) à suivre" (row_id) so a Jauge targets one
specific row, and "Champ contenant le nom" (champ_nom) to show a
label above the bar — both as dropdowns populated from the object's
actual fields/rows (previously "Champ numérique à afficher" was free
text the user had to type correctly by hand). No regression: without
row_id the widget still falls back to the latest row, exactly as
before.
Extract data_definition_options() (rows+fields for a definition) out
of routes/screens/screen_edit.py so controls_with_values.py can reuse
it server-side for the initial render; a small client-side handler
(bindJaugeDefinitionSelect) repopulates the same selects live when
the tracked object is changed without leaving the panel.
Verified live with Playwright: picking "jauge" then "Confiance" then
"value"/"name" in the panel renders an 80%-filled, green-leaning bar
labelled "Confiance" on /play — not the 20%/50% of the other rows.
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").
- 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.