Add a Font Awesome icon gallery to the properties panel, draggable onto the screen

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).
This commit is contained in:
william
2026-08-24 19:10:25 +02:00
parent 850242d1d4
commit cc0442ef5f
8 changed files with 162 additions and 0 deletions
+7
View File
@@ -17,4 +17,11 @@ def element_add(slug, screen_id):
et = types.get(int(element_type_id)) et = types.get(int(element_type_id))
widget = et["default_widget"] if et else "conteneur" widget = et["default_widget"] if et else "conteneur"
new_id = screens.add_element(slug, screen_id, widget, element_type_id) new_id = screens.add_element(slug, screen_id, widget, element_type_id)
icon_class = request.form.get("icon_class")
if widget == "icone" and icon_class:
# Posé depuis la galerie d'icônes (voir le panneau de propriétés,
# section "Icônes") : la classe Font Awesome choisie remplace celle
# posée par défaut à la création plutôt que de repartir d'une icône
# neutre que l'utilisateur devrait ensuite retaper à la main.
screens.save_element_controls(slug, new_id, {"ctrl_icon_class": icon_class})
return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id, selected=new_id)) return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id, selected=new_id))
+1
View File
@@ -73,4 +73,5 @@ def screen_edit(slug, screen_id):
animation_clips=animation_clips, animation_clips_json=json.dumps(animation_clips), animation_clips=animation_clips, animation_clips_json=json.dumps(animation_clips),
animate_css_catalog=screens.ANIMATE_CSS_CATALOG, animate_css_catalog=screens.ANIMATE_CSS_CATALOG,
animate_css_labels_json=json.dumps(screens.ANIMATE_CSS_LABELS), animate_css_labels_json=json.dumps(screens.ANIMATE_CSS_LABELS),
fontawesome_icons=screens.FONTAWESOME_ICONS,
) )
+1
View File
@@ -41,6 +41,7 @@ from .widgets.registry import WIDGETS
from .widgets.container_widgets import CONTAINER_WIDGETS from .widgets.container_widgets import CONTAINER_WIDGETS
from .widgets.layout_capable_widgets import LAYOUT_CAPABLE_WIDGETS from .widgets.layout_capable_widgets import LAYOUT_CAPABLE_WIDGETS
from .widgets.font_options import GOOGLE_FONTS_LINK from .widgets.font_options import GOOGLE_FONTS_LINK
from .widgets.fontawesome_icons import FONTAWESOME_ICONS
from .widgets.widget_meta import widget_meta from .widgets.widget_meta import widget_meta
from .widgets.controls_with_values import controls_with_values from .widgets.controls_with_values import controls_with_values
from .widgets.data_definition_options import data_definition_options from .widgets.data_definition_options import data_definition_options
+46
View File
@@ -0,0 +1,46 @@
# Sélection large mais CURATÉE d'icônes Font Awesome 5 Free (style "solid")
# pour la galerie du widget Icône (voir bulma_controls.py/registry.py et la
# section "Icônes" du panneau de propriétés dans screen_edit.html) —
# volontairement PAS le catalogue Font Awesome 5 Free complet (~1500 icônes) :
# une liste embarquée doit être garantie exacte (une classe qui n'existe pas
# affiche une case vide dans la police), donc on limite ici aux icônes dont
# le nom est sûr plutôt que de deviner l'exhaustif. N'importe quelle autre
# icône Font Awesome 5 reste utilisable en tapant directement sa classe dans
# le réglage "Icône" du panneau de propriétés.
FONTAWESOME_ICONS = [
"star", "heart", "check", "times", "plus", "minus", "search", "cog", "cogs", "wrench",
"hammer", "tools", "trash", "trash-alt", "edit", "pen", "pencil-alt", "save", "download",
"upload", "share", "share-alt", "link", "unlink", "lock", "lock-open", "key", "eye",
"eye-slash", "bell", "bell-slash", "bookmark", "flag", "flag-checkered", "thumbtack",
"filter", "sort", "sort-up", "sort-down", "list", "list-ul", "list-ol", "th", "th-list",
"th-large", "bars", "ellipsis-h", "ellipsis-v", "expand", "compress", "sync", "sync-alt",
"redo", "undo", "home", "user", "users", "user-circle", "user-plus", "user-minus",
"user-friends", "sign-in-alt", "sign-out-alt", "question-circle", "info-circle",
"exclamation-circle", "exclamation-triangle", "times-circle", "check-circle", "ban",
"shield-alt", "gem", "coins", "money-bill", "wallet", "credit-card", "gift", "trophy",
"medal", "award", "crown", "dice", "dice-d6", "dice-d20", "chess", "chess-king",
"chess-queen", "chess-knight", "chess-bishop", "chess-rook", "chess-pawn", "puzzle-piece",
"gamepad", "rocket", "bomb", "fire", "bolt", "snowflake", "sun", "moon", "cloud",
"cloud-rain", "wind", "umbrella", "tree", "leaf", "seedling", "paw", "spider", "skull",
"skull-crossbones", "map", "map-marker-alt", "map-marked-alt", "compass", "mountain",
"water", "globe", "globe-americas", "flask", "vial", "syringe", "pills", "heartbeat",
"dumbbell", "running", "walking", "swimmer", "futbol", "basketball-ball", "volleyball-ball",
"football-ball", "baseball-ball", "table-tennis", "bowling-ball", "horse", "dove", "cat",
"dog", "fish", "frog", "hippo", "otter", "kiwi-bird", "book", "book-open", "graduation-cap",
"school", "chalkboard", "chalkboard-teacher", "pencil-ruler", "ruler", "ruler-combined",
"calculator", "laptop", "desktop", "mobile-alt", "tablet-alt", "keyboard", "mouse",
"headphones", "microphone", "video", "camera", "image", "images", "film", "music", "play",
"pause", "stop", "step-forward", "step-backward", "volume-up", "volume-down", "volume-mute",
"volume-off", "envelope", "envelope-open", "comment", "comments", "comment-dots", "phone",
"phone-alt", "calendar", "calendar-alt", "calendar-check", "clock", "hourglass",
"hourglass-half", "stopwatch", "car", "car-side", "truck", "plane", "plane-departure",
"ship", "anchor", "subway", "train", "bus", "bicycle", "taxi", "road", "traffic-light",
"gas-pump", "screwdriver", "toolbox", "hard-hat", "building", "city", "industry",
"warehouse", "store", "shopping-cart", "shopping-bag", "box", "boxes", "dolly", "pallet",
"archive", "folder", "folder-open", "file", "file-alt", "file-pdf", "file-word",
"file-excel", "file-image", "file-video", "file-audio", "database", "server",
"network-wired", "wifi", "signal", "satellite", "satellite-dish", "robot", "microchip",
"memory", "hdd", "print", "cut", "paste", "copy", "clipboard", "clipboard-list",
"clipboard-check", "sticky-note", "paperclip", "paint-brush", "palette", "magic",
"fire-extinguisher", "first-aid", "briefcase", "suitcase",
]
+16
View File
@@ -315,4 +315,20 @@ WIDGETS = {
"w": 60, "h": 40, "default_content": "", "w": 60, "h": 40, "default_content": "",
"controls": BULMA_TABS_CONTROLS, "controls": BULMA_TABS_CONTROLS,
}, },
"icone": {
# La classe Font Awesome ("fa-solid fa-xxx") EST le contenu visuel de
# ce widget — posée soit en cliquant/glissant une icône depuis la
# galerie du panneau de propriétés (voir routes/elements/element_add.py,
# qui pré-remplit ce réglage), soit tapée à la main ici pour n'importe
# quelle icône Font Awesome 5 hors de la sélection curatée de la
# galerie (voir fontawesome_icons.py).
"label": "Icône", "icon": "🔤", "tag": "i", "has_content": False,
"w": 15, "h": 15, "default_content": "",
"controls": [
c_text("icon_class", "Icône (classe Font Awesome 5)", "attr:class",
default="fa-solid fa-star", placeholder="fa-solid fa-heart", group="Contenu"),
c_slider("font-size", "Taille", "style:font-size", min=10, max=160, default=32, group="Texte"),
c_color("color", "Couleur", "style:color", "#e8eaf0", group="Texte"),
],
},
} }
+12
View File
@@ -183,6 +183,18 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
#propsPanelReopenBtn{ right:24px; } #propsPanelReopenBtn{ right:24px; }
.widgetGrid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; } .widgetGrid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; }
/* Galerie d'icônes Font Awesome (panneau de propriétés) — voir
fontawesome_icons.py et addIconElement()/filterIconGallery() plus bas. */
.iconGallery{ display:grid; grid-template-columns:repeat(6, 1fr); gap:6px; max-height:220px; overflow-y:auto; margin-top:8px; }
.iconGalleryTile{
aspect-ratio:1; display:flex; align-items:center; justify-content:center;
background:var(--panel2); border:1px solid var(--border); border-radius:6px;
color:var(--text); cursor:grab; font-size:15px; padding:0;
}
.iconGalleryTile:hover{ border-color:var(--accent); color:var(--accent); }
.iconGalleryTile.dragging{ opacity:0.4; }
.iconGalleryTile.iconHidden{ display:none; }
.widgetBtnForm{ margin:0; } .widgetBtnForm{ margin:0; }
.widgetBtn{ .widgetBtn{
width:100%; display:flex; flex-direction:column; align-items:center; gap:4px; width:100%; display:flex; flex-direction:column; align-items:center; gap:4px;
+1
View File
@@ -8,6 +8,7 @@
<link rel="stylesheet" href="{{ google_fonts_link }}"> <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"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
<style> <style>
body{ margin:0; background:#000; } body{ margin:0; background:#000; }
/* ---------- Zone de jeu à proportion fixe (voir #playFrame) ---------- /* ---------- Zone de jeu à proportion fixe (voir #playFrame) ----------
+78
View File
@@ -5,6 +5,7 @@
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" href="{{ google_fonts_link }}"> <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"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
{% endblock %} {% endblock %}
{% block breadcrumb %} {% block breadcrumb %}
<li><a href="{{ url_for('game_dashboard', slug=game.slug) }}">{{ game.name }}</a></li> <li><a href="{{ url_for('game_dashboard', slug=game.slug) }}">{{ game.name }}</a></li>
@@ -161,6 +162,22 @@
<button type="button" class="button is-small" onclick="closePropsPanel()" title="Fermer">✕</button> <button type="button" class="button is-small" onclick="closePropsPanel()" title="Fermer">✕</button>
</div> </div>
<div class="floatPanelBody"> <div class="floatPanelBody">
<details class="propGroup">
<summary>🔤 Icônes (Font Awesome)</summary>
<div class="propGroupBody">
<input type="text" class="input is-small" id="iconGallerySearch" placeholder="Rechercher une icône..." oninput="filterIconGallery(this.value)">
<div class="iconGallery" id="iconGallery">
{% for slug in fontawesome_icons %}
<button type="button" class="iconGalleryTile" draggable="true"
data-icon-class="fa-solid fa-{{ slug }}" data-icon-search="{{ slug }}"
title="fa-{{ slug }}" onclick="addIconElement('fa-solid fa-{{ slug }}')">
<i class="fa-solid fa-{{ slug }}"></i>
</button>
{% endfor %}
</div>
<p class="help">Glisse une icône sur le canevas, ou clique dessus pour l'ajouter — sa couleur et sa taille se règlent ensuite dans ses propriétés ("Icône"). Sélection non exhaustive : n'importe quelle autre classe Font Awesome 5 reste utilisable en la tapant directement dans le réglage "Icône".</p>
</div>
</details>
{% if not selected_id %} {% if not selected_id %}
<div class="hint" style="padding:20px 0;">Sélectionne un élément (sur l'écran ou dans la liste) pour voir et modifier ses propriétés ici.</div> <div class="hint" style="padding:20px 0;">Sélectionne un élément (sur l'écran ou dans la liste) pour voir et modifier ses propriétés ici.</div>
{% else %} {% else %}
@@ -841,6 +858,67 @@ if (!window.__forgeFloatPanelDragBound) {
}); });
} }
// ---------- Galerie d'icônes Font Awesome (panneau de propriétés) ----------
//
// Cliquer une tuile l'ajoute directement (comme n'importe quelle tuile de
// "Ajouter un élément sur l'écran") ; la glisser jusque sur le canevas fait
// la même chose au relâchement — le drag HTML5 natif ne permet pas de créer
// un vrai élément positionné exactement sous le curseur sans aller-retour
// serveur supplémentaire, donc on se contente ici de déclencher le même
// ajout "à la position par défaut" que le clic, l'utilisateur repositionne
// ensuite comme pour tout nouvel élément.
function addIconElement(iconClass) {
var form = document.createElement('form');
form.method = 'post';
form.action = "{{ url_for('element_add', slug=game.slug, screen_id=screen.id) }}";
form.innerHTML =
'<input type="hidden" name="widget" value="icone">' +
'<input type="hidden" name="icon_class" value="' + iconClass.replace(/"/g, '&quot;') + '">';
document.body.appendChild(form);
form.requestSubmit();
}
function filterIconGallery(query) {
var q = query.trim().toLowerCase();
document.querySelectorAll('#iconGallery .iconGalleryTile').forEach(function (tile) {
var match = !q || tile.dataset.iconSearch.indexOf(q) !== -1;
tile.classList.toggle('iconHidden', !match);
});
}
if (!window.__forgeIconGalleryDragBound) {
window.__forgeIconGalleryDragBound = true;
var draggedIconClass = null;
document.addEventListener('dragstart', function (e) {
var tile = e.target.closest('.iconGalleryTile');
if (!tile) return;
draggedIconClass = tile.dataset.iconClass;
tile.classList.add('dragging');
e.dataTransfer.effectAllowed = 'copy';
try { e.dataTransfer.setData('text/plain', draggedIconClass); } catch (err) {}
});
document.addEventListener('dragend', function (e) {
var tile = e.target.closest('.iconGalleryTile');
if (tile) tile.classList.remove('dragging');
draggedIconClass = null;
});
document.addEventListener('dragover', function (e) {
if (!draggedIconClass || !e.target.closest('#canvas')) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
document.addEventListener('drop', function (e) {
if (!draggedIconClass || !e.target.closest('#canvas')) return;
e.preventDefault();
addIconElement(draggedIconClass);
draggedIconClass = null;
});
}
// ---------- Surbrillance sur le canevas de l'élément sélectionné ---------- // ---------- Surbrillance sur le canevas de l'élément sélectionné ----------
// //
// Un élément posé DIRECTEMENT sur l'écran a déjà son cadre de sélection via // Un élément posé DIRECTEMENT sur l'écran a déjà son cadre de sélection via