Make the screen editor's properties panel use native Bulma elements, not just Bulma-styled buttons

The element header/rename/id row, tabs list, position & size grid, and — most importantly — the generic control-rendering loop (used by every widget's properties panel: text, color, slider, checkbox, align, select...) now emit real field/control/label/select/checkbox/buttons-has-addons Bulma markup instead of the old custom controlRow/sliderRow/alignGroup/posGrid CSS. Removed the now-redundant custom CSS those classes used to carry.

Verified structurally safe: bindPropsAutosave/submitPropsForm already used querySelectorAll/FormData (structure-agnostic), so wrapping inputs in field/control divs doesn't affect autosave; the one sibling-dependent bit (slider oninput reading nextElementSibling) was kept adjacent inside its wrapper. Confirmed end-to-end with a live save round-trip through the actual route (Bulma modifier + free-style values both persist correctly) and every widget type's panel still renders 200 OK.

The flow-graph node editor (bottom panel) is not converted yet — part of its markup is built dynamically in JS (renderFlowConditionClauses), so reskinning it means updating the JS templates in lockstep with the HTML, which is more work/risk than this pass; noting it as the next piece.
This commit is contained in:
william
2026-08-24 13:30:17 +02:00
parent 25a331830b
commit 8da5715e5b
2 changed files with 123 additions and 88 deletions
+6 -14
View File
@@ -284,23 +284,15 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
.containerContentGroup{ border-color:#3d6b3d; background:#152015; }
.containerContentGroup > summary{ color:#8fd98f; }
.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/.controlRow/.sizeField/.sliderRow/.alignGroup : Bulma fournit
déjà la mise en page (columns/field/control/buttons has-addons, voir
screen_edit.html) — ce qui reste ici n'est que ce que Bulma ne fournit
pas nativement : l'espacement propre à un curseur (slider) et son
pastille de valeur affichée à côté. */
.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; }
.sliderRow{ display:flex; align-items:center; gap:10px; }
.sliderRow input[type=range]{ flex:1; }
.sliderValue{ font-size:12px; color:var(--text-dim); min-width:44px; text-align:right; }
.alignGroup{ display:flex; gap:6px; }
.alignOption{ flex:1; text-align:center; padding:8px 0; border:1px solid var(--border); border-radius:6px; cursor:pointer; }
.alignOption input{ display:none; }
.alignOption.active{ border-color:var(--accent); background:rgba(91,140,255,.15); }
.sliderValue{ min-width:44px; text-align:right; }
input[type=color]{ width:52px; height:34px; padding:2px; }
.propsPanel .sectionDivider, .builderPanelRight .sectionDivider{ border:none; border-top:1px solid var(--border); margin:14px 0; }
+117 -74
View File
@@ -151,19 +151,19 @@
{% if el %}
{% set w = widgets.get(el.widget, {}) %}
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<h2 style="margin:0;">{{ w.get('icon','❔') }} {{ el|elabel }}{% if el.name %} <span class="hint">— {{ w.get('label', el.widget) }}</span>{% endif %}</h2>
<h2 class="subtitle is-6" style="margin:0;">{{ w.get('icon','❔') }} {{ el|elabel }}{% if el.name %} <span class="has-text-grey">— {{ w.get('label', el.widget) }}</span>{% endif %}</h2>
<form method="post" action="{{ url_for('element_delete', slug=game.slug, element_id=el.id) }}"
onsubmit="return confirm('Supprimer cet élément ?');">
<button type="submit" class="smallBtn button is-small is-danger">🗑️</button>
<button type="submit" class="button is-small is-danger">🗑️</button>
</form>
</div>
<form method="post" action="{{ url_for('element_rename', slug=game.slug, element_id=el.id) }}" class="fieldRow" style="align-items:center; margin:6px 0;">
<input type="text" name="name" value="{{ el.name or '' }}" placeholder="Donne un nom à cet élément" style="flex:1;">
<button type="submit" class="smallBtn button is-small">Nommer</button>
<form method="post" action="{{ url_for('element_rename', slug=game.slug, element_id=el.id) }}" class="field has-addons" style="margin:6px 0;">
<div class="control is-expanded"><input class="input" type="text" name="name" value="{{ el.name or '' }}" placeholder="Donne un nom à cet élément"></div>
<div class="control"><button type="submit" class="button is-small">Nommer</button></div>
</form>
<div class="fieldRow" style="align-items:center; margin:6px 0;">
<label style="width:auto; margin-right:6px;">ID</label>
<input type="text" value="elt-{{ el.id }}" readonly style="flex:1; opacity:0.7;">
<div class="field has-addons" style="margin:6px 0;">
<div class="control"><a class="button is-small is-static">ID</a></div>
<div class="control is-expanded"><input class="input" type="text" value="elt-{{ el.id }}" readonly style="opacity:0.7;"></div>
</div>
{% if el.widget in container_widgets %}
@@ -191,27 +191,27 @@
<summary>🗂️ Onglets{{ ' (' ~ tabs|length ~ ')' if tabs else '' }}</summary>
<div class="propGroupBody">
{% if tabs %}
<div class="list">
{% for tab in tabs %}
<div class="listRow" style="display:flex; flex-direction:column; gap:6px;">
<form method="post" action="{{ url_for('element_rename', slug=game.slug, element_id=tab.id) }}" class="fieldRow" style="align-items:center; margin:0;">
<input type="text" name="name" value="{{ tab.name or ('Onglet ' ~ loop.index) }}" placeholder="Nom de l'onglet" style="flex:1;">
<button type="submit" class="smallBtn button is-small">Renommer</button>
</form>
<div class="fieldRow" style="align-items:center; margin:0;">
<a class="smallBtn button is-small" style="flex:1; text-align:center;" href="{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id, selected=tab.id) }}">📥 Contenu de l'onglet</a>
{% for tab in tabs %}
<div class="box" style="padding:10px; display:flex; flex-direction:column; gap:6px;">
<form method="post" action="{{ url_for('element_rename', slug=game.slug, element_id=tab.id) }}" class="field has-addons" style="margin:0;">
<div class="control is-expanded"><input class="input is-small" type="text" name="name" value="{{ tab.name or ('Onglet ' ~ loop.index) }}" placeholder="Nom de l'onglet"></div>
<div class="control"><button type="submit" class="button is-small">Renommer</button></div>
</form>
<div class="field has-addons" style="margin:0;">
<div class="control is-expanded"><a class="button is-small is-fullwidth" href="{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id, selected=tab.id) }}">📥 Contenu de l'onglet</a></div>
<div class="control">
<form method="post" action="{{ url_for('element_delete', slug=game.slug, element_id=tab.id) }}" onsubmit="return confirm('Supprimer cet onglet et tout son contenu ?');">
<button type="submit" class="smallBtn button is-small is-danger">🗑️</button>
<button type="submit" class="button is-small is-danger">🗑️</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
{% else %}
<p class="hint">Aucun onglet pour l'instant.</p>
<p class="help">Aucun onglet pour l'instant.</p>
{% endif %}
<form method="post" action="{{ url_for('element_tab_add', slug=game.slug, element_id=el.id) }}" style="margin-top:8px;">
<button type="submit" class="smallBtn button is-small is-primary" style="width:100%;">➕ Ajouter un onglet</button>
<button type="submit" class="button is-small is-primary is-fullwidth">➕ Ajouter un onglet</button>
</form>
</div>
</details>
@@ -225,35 +225,63 @@
<details class="propGroup">
<summary>Position &amp; taille</summary>
<div class="propGroupBody">
<div class="posGrid">
<label>X (%)<input type="number" id="pos-x" min="0" max="100" value="{{ el.x }}" {{ 'disabled' if el.parent_id else '' }}></label>
<label>Y (%)<input type="number" id="pos-y" min="0" max="100" value="{{ el.y }}" {{ 'disabled' if el.parent_id else '' }}></label>
<div class="columns is-multiline posGrid">
<div class="column is-half">
<div class="field"><label class="label is-small">X (%)</label>
<div class="control"><input class="input is-small" type="number" id="pos-x" min="0" max="100" value="{{ el.x }}" {{ 'disabled' if el.parent_id else '' }}></div>
</div>
</div>
<div class="column is-half">
<div class="field"><label class="label is-small">Y (%)</label>
<div class="control"><input class="input is-small" type="number" id="pos-y" min="0" max="100" value="{{ el.y }}" {{ 'disabled' if el.parent_id else '' }}></div>
</div>
</div>
{% if el.parent_id %}
<label>Largeur
<div class="sizeField">
<input type="number" name="ctrl_child-width_value" min="0" value="{{ size_ctrls['child-width'].value.value or '' }}" placeholder="auto">
<select name="ctrl_child-width_unit">
<option value="%" {{ 'selected' if size_ctrls['child-width'].value.unit == '%' else '' }}>%</option>
<option value="px" {{ 'selected' if size_ctrls['child-width'].value.unit == 'px' else '' }}>px</option>
</select>
<div class="column is-half">
<div class="field"><label class="label is-small">Largeur</label>
<div class="field has-addons sizeField">
<div class="control is-expanded"><input class="input is-small" type="number" name="ctrl_child-width_value" min="0" value="{{ size_ctrls['child-width'].value.value or '' }}" placeholder="auto"></div>
<div class="control">
<div class="select is-small">
<select name="ctrl_child-width_unit">
<option value="%" {{ 'selected' if size_ctrls['child-width'].value.unit == '%' else '' }}>%</option>
<option value="px" {{ 'selected' if size_ctrls['child-width'].value.unit == 'px' else '' }}>px</option>
</select>
</div>
</div>
</div>
</div>
</label>
<label>Hauteur
<div class="sizeField">
<input type="number" name="ctrl_child-height_value" min="0" value="{{ size_ctrls['child-height'].value.value or '' }}" placeholder="auto">
<select name="ctrl_child-height_unit">
<option value="%" {{ 'selected' if size_ctrls['child-height'].value.unit == '%' else '' }}>%</option>
<option value="px" {{ 'selected' if size_ctrls['child-height'].value.unit == 'px' else '' }}>px</option>
</select>
</div>
<div class="column is-half">
<div class="field"><label class="label is-small">Hauteur</label>
<div class="field has-addons sizeField">
<div class="control is-expanded"><input class="input is-small" type="number" name="ctrl_child-height_value" min="0" value="{{ size_ctrls['child-height'].value.value or '' }}" placeholder="auto"></div>
<div class="control">
<div class="select is-small">
<select name="ctrl_child-height_unit">
<option value="%" {{ 'selected' if size_ctrls['child-height'].value.unit == '%' else '' }}>%</option>
<option value="px" {{ 'selected' if size_ctrls['child-height'].value.unit == 'px' else '' }}>px</option>
</select>
</div>
</div>
</div>
</div>
</label>
</div>
{% else %}
<label>Largeur (%)<input type="number" id="pos-w" min="1" max="100" value="{{ el.width }}"></label>
<label>Hauteur (%)<input type="number" id="pos-h" min="1" max="100" value="{{ el.height }}"></label>
<div class="column is-half">
<div class="field"><label class="label is-small">Largeur (%)</label>
<div class="control"><input class="input is-small" type="number" id="pos-w" min="1" max="100" value="{{ el.width }}"></div>
</div>
</div>
<div class="column is-half">
<div class="field"><label class="label is-small">Hauteur (%)</label>
<div class="control"><input class="input is-small" type="number" id="pos-h" min="1" max="100" value="{{ el.height }}"></div>
</div>
</div>
{% endif %}
</div>
{% if el.parent_id %}
<p class="hint">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.</p>
<p class="help">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.</p>
{% endif %}
</div>
</details>
@@ -271,50 +299,65 @@
{% set ns.last_group = c.group %}
{% set ns.started = True %}
{% endif %}
<div class="controlRow">
<label>{{ c.label }}</label>
{% if c.type in ('text', 'url') %}
<input type="text" name="ctrl_{{ c.key }}" value="{{ c.value }}" placeholder="{{ c.placeholder or '' }}" class="{{ 'uploadTextField' if c.allow_upload else '' }}" id="field-{{ c.key }}">
{% if c.allow_upload %}
<div class="uploadRow">
<input type="file" class="uploadInput" accept="{{ c.upload_accept }}" data-target="field-{{ c.key }}">
<span class="hint uploadStatus"></span>
<div class="field controlRow">
{% if c.type in ('checkbox', 'toggle_token') %}
<div class="control">
<label class="checkbox">
<input type="checkbox" name="ctrl_{{ c.key }}" {{ 'checked' if c.value else '' }}>
{{ c.label }}
</label>
</div>
<p class="hint">Colle une adresse (URL) ci-dessus, ou choisis un fichier sur ton ordinateur — il sera hébergé automatiquement avec ce jeu.</p>
{% else %}
<label class="label is-small">{{ c.label }}</label>
{% if c.type in ('text', 'url') %}
<div class="control">
<input class="input {{ 'uploadTextField' if c.allow_upload else '' }}" type="text" name="ctrl_{{ c.key }}" value="{{ c.value }}" placeholder="{{ c.placeholder or '' }}" id="field-{{ c.key }}">
</div>
{% if c.allow_upload %}
<div class="control uploadRow">
<input type="file" class="uploadInput" accept="{{ c.upload_accept }}" data-target="field-{{ c.key }}">
<span class="help uploadStatus"></span>
</div>
<p class="help">Colle une adresse (URL) ci-dessus, ou choisis un fichier sur ton ordinateur — il sera hébergé automatiquement avec ce jeu.</p>
{% endif %}
{% elif c.type == 'textarea' %}
<textarea name="ctrl_{{ c.key }}" rows="4" placeholder="{{ c.placeholder or '' }}">{{ c.value }}</textarea>
<div class="control"><textarea class="textarea" name="ctrl_{{ c.key }}" rows="4" placeholder="{{ c.placeholder or '' }}">{{ c.value }}</textarea></div>
{% elif c.type == 'color' %}
<input type="color" name="ctrl_{{ c.key }}" value="{{ c.value or c.default }}">
<div class="control"><input class="input" type="color" name="ctrl_{{ c.key }}" value="{{ c.value or c.default }}"></div>
{% elif c.type == 'slider' %}
<div class="sliderRow">
<div class="control sliderRow">
<input type="range" name="ctrl_{{ c.key }}" min="{{ c.min }}" max="{{ c.max }}" step="{{ c.step }}" value="{{ c.value }}"
oninput="this.nextElementSibling.textContent = this.value + '{{ c.unit }}'">
<span class="sliderValue">{{ c.value }}{{ c.unit }}</span>
<span class="sliderValue tag">{{ c.value }}{{ c.unit }}</span>
</div>
{% elif c.type == 'scale' %}
<div class="sliderRow">
<div class="control sliderRow">
<input type="range" name="ctrl_{{ c.key }}" min="{{ c.min }}" max="{{ c.max }}" step="{{ c.step }}" value="{{ c.value }}"
oninput="this.nextElementSibling.textContent = 'x' + this.value">
<span class="sliderValue">x{{ c.value }}</span>
<span class="sliderValue tag">x{{ c.value }}</span>
</div>
{% elif c.type in ('checkbox', 'toggle_token') %}
<input type="checkbox" name="ctrl_{{ c.key }}" {{ 'checked' if c.value else '' }}>
{% elif c.type == 'align' %}
<div class="alignGroup">
{% for val, icon in [('left','⬅️'),('center','⬛'),('right','➡️'),('justify','☰')] %}
<label class="alignOption {{ 'active' if c.value == val else '' }}">
<input type="radio" name="ctrl_{{ c.key }}" value="{{ val }}" {{ 'checked' if c.value == val else '' }} onchange="syncAlign(this)">
{{ icon }}
</label>
{% endfor %}
<div class="control">
<div class="buttons has-addons alignGroup">
{% for val, icon in [('left','⬅️'),('center','⬛'),('right','➡️'),('justify','☰')] %}
<label class="button is-small alignOption {{ 'is-primary active' if c.value == val else '' }}">
<input type="radio" name="ctrl_{{ c.key }}" value="{{ val }}" {{ 'checked' if c.value == val else '' }} onchange="syncAlign(this)" style="display:none;">
{{ icon }}
</label>
{% endfor %}
</div>
</div>
{% elif c.type in ('select', 'preset') %}
<select name="ctrl_{{ c.key }}" id="field-{{ c.key }}">
{% for opt in c.options %}
<option value="{{ opt[0] }}" {{ 'selected' if c.value == opt[0] else '' }}>{{ opt[1] }}</option>
{% endfor %}
</select>
<div class="control">
<div class="select is-fullwidth">
<select name="ctrl_{{ c.key }}" id="field-{{ c.key }}">
{% for opt in c.options %}
<option value="{{ opt[0] }}" {{ 'selected' if c.value == opt[0] else '' }}>{{ opt[1] }}</option>
{% endfor %}
</select>
</div>
</div>
{% endif %}
{% endif %}
</div>
{% endfor %}
@@ -1178,8 +1221,8 @@ function onDataDefinitionChange(sel) {
document.querySelectorAll('.targetDefinitionSelect').forEach(onDataDefinitionChange);
function syncAlign(radio) {
radio.closest('.alignGroup').querySelectorAll('.alignOption').forEach(function(o){ o.classList.remove('active'); });
radio.closest('.alignOption').classList.add('active');
radio.closest('.alignGroup').querySelectorAll('.alignOption').forEach(function(o){ o.classList.remove('active', 'is-primary'); });
radio.closest('.alignOption').classList.add('active', 'is-primary');
}
// Choix du format d'aperçu (Portrait/Paysage/Carré) : mémorisé côté serveur