Move element tree above the add-element menus, make containers collapsible

The "Éléments de cet écran" tree now sits above "Ajouter un élément sur
l'écran" / "Mes éléments de jeu" so it's the first thing visible in the
left panel.

Each container row gets a disclosure triangle that collapses/expands its
children, independent from the row's link (which still selects the
element and shows its properties whether collapsed or expanded). The
collapsed state is remembered per element in localStorage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-23 20:17:21 +02:00
co-authored by Claude Sonnet 5
parent bb79f2f93d
commit 386e93d9c6
2 changed files with 78 additions and 23 deletions
+12 -2
View File
@@ -160,10 +160,20 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
pour rester lisible même avec beaucoup d'éléments. */ pour rester lisible même avec beaucoup d'éléments. */
.elementTree{ display:flex; flex-direction:column; gap:1px; max-height:55vh; overflow-y:auto; margin-bottom:14px; } .elementTree{ display:flex; flex-direction:column; gap:1px; max-height:55vh; overflow-y:auto; margin-bottom:14px; }
.treeNode{ display:flex; flex-direction:column; } .treeNode{ display:flex; flex-direction:column; }
.treeRow{ display:flex; align-items:center; gap:2px; }
.treeToggle{
flex:0 0 auto; width:16px; height:16px; padding:0; border:none; background:none; cursor:pointer;
color:var(--text-dim); font-size:10px; display:flex; align-items:center; justify-content:center;
transition:transform .12s ease;
}
.treeToggle:hover{ color:var(--text); }
.treeToggleSpacer{ flex:0 0 auto; width:16px; height:16px; }
.treeNode.collapsed > .treeRow > .treeToggle{ transform:rotate(-90deg); }
.treeNode.collapsed > .treeChildren{ display:none; }
.treeRowLink{ .treeRowLink{
display:flex; align-items:center; gap:6px; padding:4px 6px; border-radius:5px; flex:1; display:flex; align-items:center; gap:6px; padding:4px 6px; border-radius:5px;
text-decoration:none; color:var(--text); font-size:12.5px; line-height:1.5; text-decoration:none; color:var(--text); font-size:12.5px; line-height:1.5;
white-space:nowrap; overflow:hidden; white-space:nowrap; overflow:hidden; min-width:0;
} }
.treeRowLink:hover{ background:var(--panel2); } .treeRowLink:hover{ background:var(--panel2); }
.treeRowLink.selected{ background:var(--accent); color:#0b0e14; font-weight:600; } .treeRowLink.selected{ background:var(--accent); color:#0b0e14; font-weight:600; }
+66 -21
View File
@@ -30,15 +30,22 @@
{% endif %} {% endif %}
{% macro render_tree_node(el) %} {% macro render_tree_node(el) %}
<div class="treeNode"> {% set kids = children_of.get(el.id, []) %}
<a class="treeRowLink {{ 'selected' if el.id == selected_id else '' }}" <div class="treeNode" data-element-id="{{ el.id }}">
href="{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id, selected=el.id) }}" <div class="treeRow">
data-element-id="{{ el.id }}" {% if kids %}
oncontextmenu="return showElementContextMenu(event, {{ el.id }});"> <button type="button" class="treeToggle" data-node-id="{{ el.id }}" aria-label="Réduire / déplier">▾</button>
<span class="treeIcon">{{ widgets.get(el.widget, {}).get('icon', '❔') }}</span> {% else %}
<span class="treeLabel">{{ el|elabel }}</span> <span class="treeToggleSpacer"></span>
</a> {% endif %}
{% set kids = children_of.get(el.id, []) %} <a class="treeRowLink {{ 'selected' if el.id == selected_id else '' }}"
href="{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id, selected=el.id) }}"
data-element-id="{{ el.id }}"
oncontextmenu="return showElementContextMenu(event, {{ el.id }});">
<span class="treeIcon">{{ widgets.get(el.widget, {}).get('icon', '❔') }}</span>
<span class="treeLabel">{{ el|elabel }}</span>
</a>
</div>
{% if kids %} {% if kids %}
<div class="treeChildren"> <div class="treeChildren">
{% for child in kids %} {% for child in kids %}
@@ -53,6 +60,17 @@
{% set el = elements|selectattr("id", "equalto", selected_id)|first if selected_id else none %} {% 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) --> <!-- Panneau gauche : ajouter un élément (icônes, sans jargon technique) -->
<div class="builderPanel"> <div class="builderPanel">
<h2>Éléments de cet écran</h2>
{% if top_level_elements %}
<div class="elementTree">
{% for el in top_level_elements %}
{{ render_tree_node(el) }}
{% endfor %}
</div>
{% else %}
<p class="hint">Aucun élément pour l'instant.</p>
{% endif %}
<details class="propGroup"> <details class="propGroup">
<summary>Ajouter un élément sur l'écran</summary> <summary>Ajouter un élément sur l'écran</summary>
<div class="propGroupBody"> <div class="propGroupBody">
@@ -95,17 +113,6 @@
{% endif %} {% endif %}
</div> </div>
</details> </details>
<h2>Éléments de cet écran</h2>
{% if top_level_elements %}
<div class="elementTree">
{% for el in top_level_elements %}
{{ render_tree_node(el) }}
{% endfor %}
</div>
{% else %}
<p class="hint">Aucun élément pour l'instant.</p>
{% endif %}
</div> </div>
<!-- Menu contextuel (clic droit sur un élément de l'arborescence) --> <!-- Menu contextuel (clic droit sur un élément de l'arborescence) -->
@@ -579,6 +586,7 @@ function saveGeometry(id, x, y, width, height) {
var SCREEN_EDIT_URL = "{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id) }}"; var SCREEN_EDIT_URL = "{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id) }}";
var CURRENT_SELECTED_ID = {{ selected_id or 'null' }}; var CURRENT_SELECTED_ID = {{ selected_id or 'null' }};
var GAME_SLUG = "{{ game.slug }}"; var GAME_SLUG = "{{ game.slug }}";
var SCREEN_ID = {{ screen.id }};
// ---------- Menu contextuel (clic droit) de l'arborescence des éléments ---------- // ---------- Menu contextuel (clic droit) de l'arborescence des éléments ----------
// //
@@ -626,6 +634,42 @@ if (!window.__forgeContextMenuBound) {
document.addEventListener('scroll', hideElementContextMenu, true); document.addEventListener('scroll', hideElementContextMenu, true);
} }
// ---------- Arborescence : replier/déplier un conteneur ----------
//
// Le triangle (.treeToggle) replie/déplie SANS toucher à la sélection : le
// nom de l'élément reste un lien à part (.treeRowLink) qui, lui, sélectionne
// toujours l'élément et affiche ses réglages — cliquer sur un conteneur
// replié donne donc bien accès à ses propriétés, comme demandé. L'état
// replié/déplié est mémorisé par élément dans localStorage pour survivre à
// un changement de sélection ou un rechargement de l'éditeur.
function treeCollapseKey(nodeId) {
return 'forgeTreeCollapsed:' + GAME_SLUG + ':' + SCREEN_ID + ':' + nodeId;
}
function restoreTreeCollapsedState() {
document.querySelectorAll('.elementTree .treeNode').forEach(function (node) {
var id = node.dataset.elementId;
var collapsed = false;
try { collapsed = localStorage.getItem(treeCollapseKey(id)) === '1'; } catch (e) {}
node.classList.toggle('collapsed', collapsed);
});
}
if (!window.__forgeTreeToggleBound) {
window.__forgeTreeToggleBound = true;
document.addEventListener('click', function (e) {
var btn = e.target.closest('.treeToggle');
if (!btn) return;
e.preventDefault();
e.stopPropagation();
var node = btn.closest('.treeNode');
var collapsed = node.classList.toggle('collapsed');
try { localStorage.setItem(treeCollapseKey(node.dataset.elementId), collapsed ? '1' : '0'); } catch (err) {}
});
}
restoreTreeCollapsedState();
// ---------- Navigation dans l'éditeur sans AUCUN rechargement de page ---------- // ---------- Navigation dans l'éditeur sans AUCUN rechargement de page ----------
// //
// Se déplacer dans le canevas (glisser, redimensionner, cliquer un autre // Se déplacer dans le canevas (glisser, redimensionner, cliquer un autre
@@ -676,7 +720,7 @@ function refreshCanvasOnly() {
// focus/curseur pendant qu'on modifie un réglage. // focus/curseur pendant qu'on modifie un réglage.
var newTree = doc.querySelector('.builderPanel .elementTree'); var newTree = doc.querySelector('.builderPanel .elementTree');
var curTree = document.querySelector('.builderPanel .elementTree'); var curTree = document.querySelector('.builderPanel .elementTree');
if (newTree && curTree) curTree.innerHTML = newTree.innerHTML; if (newTree && curTree) { curTree.innerHTML = newTree.innerHTML; restoreTreeCollapsedState(); }
}); });
} }
@@ -880,6 +924,7 @@ function initBuilderPanel() {
bindPropsAutosave(); bindPropsAutosave();
bindJaugeDefinitionSelect(); bindJaugeDefinitionSelect();
restoreTreeCollapsedState();
} }
// Jauge : quand on change l'objet de données à suivre SANS quitter le // Jauge : quand on change l'objet de données à suivre SANS quitter le