Reskin the whole admin UI (not just game widgets) with Bulma

Loads Bulma 1.0.2 in base.html (data-theme="dark" for its native dark palette) for every page that extends it, plus play.html directly. Converts every admin template — index, game dashboard, screens list, element types, object new/edit, data form/list — to real Bulma markup: navbar, breadcrumb component, box/card, field/control/input/select, table, notification, buttons, and a native Bulma modal for the data-row detail popup. Forge's own style.css keeps only what Bulma doesn't cover (the fixed-viewport builder/object-edit layouts, the element tree, canvas, flow-graph editor) and now acts as a secondary/override layer rather than a competing design system, matching how per-element inline customization already overrides widget defaults.

screen_edit.html (the 3-panel screen builder) gets the same navbar/breadcrumb/button treatment plus its top rename form, but its flow-graph node editor, canvas and element tree keep their existing custom styling — several of their inputs have JS relying on exact DOM sibling structure (e.g. slider oninput reading nextElementSibling) or Bulma's own select/wrapper requirement, and reskinning them without a browser to verify against risked silently breaking the app's most complex feature. Buttons and headings there are still fully converted (safe, purely additive class changes).

Verified: full test suite green, every route smoke-rendered 200 OK via the test client after the change.
This commit is contained in:
william
2026-08-24 13:19:02 +02:00
parent 38f33d88d9
commit 25a331830b
11 changed files with 454 additions and 352 deletions
+27 -35
View File
@@ -5,28 +5,20 @@
{% block extra_head %}
<link rel="stylesheet" href="{{ google_fonts_link }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
<!-- Bulma : moteur de style par défaut de certains widgets (Bouton, Titre,
Conteneur en carte) — voir screens/widgets/control_groups/bulma_controls.py.
Chargé APRÈS style.css : les styles en ligne posés par le panneau de
propriétés (couleur, taille personnalisées...) gagnent de toute façon
toujours contre une classe CSS, donc l'ordre ne change rien au résultat
— Bulma ne fournit qu'un habillage par défaut, jamais un dernier mot. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
{% endblock %}
{% block breadcrumb %}
<a class="crumb" href="{{ url_for('game_dashboard', slug=game.slug) }}">{{ game.name }}</a>
<span class="crumb-sep">/</span>
<a class="crumb" href="{{ url_for('screens_list', slug=game.slug) }}">Écrans</a>
<span class="crumb-sep">/</span><span class="crumb">{{ screen.name }}</span>
<li><a href="{{ url_for('game_dashboard', slug=game.slug) }}">{{ game.name }}</a></li>
<li><a href="{{ url_for('screens_list', slug=game.slug) }}">Écrans</a></li>
<li class="is-active"><a>{{ screen.name }}</a></li>
{% endblock %}
{% block content %}
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<form method="post" action="{{ url_for('screen_rename', slug=game.slug, screen_id=screen.id) }}" class="fieldRow" style="align-items:center; margin:0;">
<input type="text" name="name" value="{{ screen.name }}" required style="font-size:16px; font-weight:700; width:auto;">
<button type="submit" class="smallBtn">Renommer</button>
<form method="post" action="{{ url_for('screen_rename', slug=game.slug, screen_id=screen.id) }}" class="field has-addons" style="align-items:center; margin:0;">
<div class="control"><input class="input" type="text" name="name" value="{{ screen.name }}" required style="font-size:16px; font-weight:700; width:auto;"></div>
<div class="control"><button type="submit" class="button is-small">Renommer</button></div>
</form>
{% if not screen.is_template %}
<a class="smallBtn primary" href="{{ url_for('game_play', slug=game.slug) }}" target="_blank">▶️ Jouer depuis le début</a>
<a class="button is-small is-primary" href="{{ url_for('game_play', slug=game.slug) }}" target="_blank">▶️ Jouer depuis le début</a>
{% endif %}
</div>
@@ -67,7 +59,7 @@
{% set el = elements|selectattr("id", "equalto", selected_id)|first if selected_id else none %}
<!-- Panneau gauche : ajouter un élément (icônes, sans jargon technique) -->
<div class="builderPanel">
<h2>Éléments de cet écran</h2>
<h2 class="subtitle is-6">Éléments de cet écran</h2>
{% if top_level_elements %}
<div class="elementTree">
{% for el in top_level_elements %}
@@ -162,12 +154,12 @@
<h2 style="margin:0;">{{ w.get('icon','❔') }} {{ el|elabel }}{% if el.name %} <span class="hint">— {{ 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 danger">🗑️</button>
<button type="submit" class="smallBtn 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">Nommer</button>
<button type="submit" class="smallBtn button is-small">Nommer</button>
</form>
<div class="fieldRow" style="align-items:center; margin:6px 0;">
<label style="width:auto; margin-right:6px;">ID</label>
@@ -204,12 +196,12 @@
<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">Renommer</button>
<button type="submit" class="smallBtn button is-small">Renommer</button>
</form>
<div class="fieldRow" style="align-items:center; margin:0;">
<a class="smallBtn" 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>
<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>
<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 danger">🗑️</button>
<button type="submit" class="smallBtn button is-small is-danger">🗑️</button>
</form>
</div>
</div>
@@ -219,7 +211,7 @@
<p class="hint">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 primary" style="width:100%;">➕ Ajouter un onglet</button>
<button type="submit" class="smallBtn button is-small is-primary" style="width:100%;">➕ Ajouter un onglet</button>
</form>
</div>
</details>
@@ -356,16 +348,16 @@
</div>
<div class="flowPanelBody" id="flowPanelBody">
<div class="flowToolbar">
<button type="button" class="smallBtn" onclick="openNodeForm('trigger')">➕ Déclencheur</button>
<button type="button" class="smallBtn" onclick="openNodeForm('condition')">➕ Condition</button>
<button type="button" class="smallBtn" onclick="openNodeForm('action')">➕ Action</button>
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('trigger')">➕ Déclencheur</button>
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('condition')">➕ Condition</button>
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('action')">➕ Action</button>
<span class="hint">Clique un point de sortie d'un nœud, puis le point d'entrée d'un autre nœud, pour les relier. Reclique une connexion pour la supprimer.</span>
</div>
<div id="nodeForm" class="nodeForm" style="display:none;">
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<strong id="nodeFormTitle">Nouveau nœud</strong>
<button type="button" class="smallBtn" onclick="closeNodeForm()">✕</button>
<button type="button" class="smallBtn button is-small" onclick="closeNodeForm()">✕</button>
</div>
<div id="nodeFormTrigger" style="display:none;">
@@ -409,7 +401,7 @@
</select>
</label>
<div id="nf-cond-extra-clauses"></div>
<button type="button" class="smallBtn" onclick="addFlowConditionClause()">+ Ajouter une condition</button>
<button type="button" class="smallBtn button is-small" onclick="addFlowConditionClause()">+ Ajouter une condition</button>
</div>
{% endif %}
</div>
@@ -497,7 +489,7 @@
</div>
</div>
<div class="actionsRow"><button type="button" class="smallBtn primary" onclick="submitNodeForm()">+ Ajouter ce nœud</button></div>
<div class="actionsRow"><button type="button" class="smallBtn button is-small is-primary" onclick="submitNodeForm()">+ Ajouter ce nœud</button></div>
</div>
<div class="flowCanvasWrap">
@@ -532,13 +524,13 @@
{% endfor %}
</select>
</label>
<button type="button" class="smallBtn" onclick="addAnimTrack()">➕ Ajouter un clip</button>
<button type="button" class="smallBtn button is-small" onclick="addAnimTrack()">➕ Ajouter un clip</button>
</div>
<div id="animClipForm" class="nodeForm" style="display:none;">
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<strong id="animClipFormTitle">Clip d'animation</strong>
<button type="button" class="smallBtn" onclick="closeAnimClipForm()">✕</button>
<button type="button" class="smallBtn button is-small" onclick="closeAnimClipForm()">✕</button>
</div>
<label>Type
@@ -564,7 +556,7 @@
<div id="anim-clip-custom-fields" style="display:none;">
<div id="anim-clip-keyframes"></div>
<button type="button" class="smallBtn" onclick="addAnimKeyframeRow()">+ Ajouter une image-clé</button>
<button type="button" class="smallBtn button is-small" onclick="addAnimKeyframeRow()">+ Ajouter une image-clé</button>
</div>
<div class="fieldRow">
@@ -599,9 +591,9 @@
</label>
<div class="actionsRow">
<button type="button" class="smallBtn primary" onclick="saveAnimClipForm()">Enregistrer</button>
<button type="button" class="smallBtn" onclick="previewAnimClip()">▶ Aperçu</button>
<button type="button" class="smallBtn danger" onclick="deleteAnimClipForm()">🗑️ Supprimer</button>
<button type="button" class="smallBtn button is-small is-primary" onclick="saveAnimClipForm()">Enregistrer</button>
<button type="button" class="smallBtn button is-small" onclick="previewAnimClip()">▶ Aperçu</button>
<button type="button" class="smallBtn button is-small is-danger" onclick="deleteAnimClipForm()">🗑️ Supprimer</button>
</div>
</div>
@@ -1546,7 +1538,7 @@ function renderFlowConditionClauses() {
CONDITION_OPERATORS_HTML +
'</select></label>' +
'<label>Valeur de comparaison<input type="text" id="nf-cond-extra-value-' + idx + '" placeholder="ex. 1, vrai, 10..." value="' + (clause.value || '').replace(/"/g, '&quot;') + '" onchange="flowConditionExtraClauses[' + idx + '].value = this.value"></label>' +
'<button type="button" class="smallBtn" onclick="removeFlowConditionClause(' + idx + ')">Retirer cette condition</button>' +
'<button type="button" class="smallBtn button is-small" onclick="removeFlowConditionClause(' + idx + ')">Retirer cette condition</button>' +
'</div>';
}).join('');
flowConditionExtraClauses.forEach(function(clause, idx) {