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:
+117
-74
@@ -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 & 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
|
||||
|
||||
Reference in New Issue
Block a user