first commit
Build and deploy / build-and-push (push) Successful in 17s
Build and deploy / deploy (push) Successful in 10s

This commit is contained in:
william
2026-08-21 16:23:49 +02:00
committed by william
commit 3f4ebc4527
248 changed files with 10180 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Catalogue de widgets et constructeurs de réglages ("controls")."""
+1
View File
@@ -0,0 +1 @@
BORDER_STYLE_OPTIONS = [("solid", "Trait plein"), ("dashed", "Tirets"), ("dotted", "Pointillés"), ("none", "Aucune")]
+1
View File
@@ -0,0 +1 @@
CONTAINER_WIDGETS = {"conteneur", "repeteur_donnees", "groupe_champs", "superposition"}
@@ -0,0 +1 @@
"""Groupes de réglages ("controls") réutilisés par plusieurs widgets."""
@@ -0,0 +1,20 @@
from ..controls.c_slider import c_slider
from ..controls.c_color import c_color
from ..controls.c_select import c_select
from ..border_style_options import BORDER_STYLE_OPTIONS
# Les épaisseurs "par côté" ci-dessous sont volontairement placées APRÈS
# "border-width" (tous les côtés) dans cette même liste : pour une même
# propriété CSS, la déclaration qui apparaît EN DERNIER dans l'attribut
# style l'emporte — un réglage par côté prime donc sur le réglage global
# quand les deux sont utilisés ensemble. La couleur et le style de bordure
# restent communs aux 4 côtés.
BORDER_CONTROLS = [
c_slider("border-width", "Épaisseur de la bordure, tous les côtés", "style:border-width", min=0, max=20, default=0, group="Bordure"),
c_color("border-color", "Couleur de la bordure", "style:border-color", "#2b3140", group="Bordure"),
c_select("border-style", "Style de la bordure", "style:border-style", BORDER_STYLE_OPTIONS, default="solid", group="Bordure"),
c_slider("border-top-width", "Épaisseur — haut", "style:border-top-width", min=0, max=20, default=0, group="Bordure"),
c_slider("border-right-width", "Épaisseur — droite", "style:border-right-width", min=0, max=20, default=0, group="Bordure"),
c_slider("border-bottom-width", "Épaisseur — bas", "style:border-bottom-width", min=0, max=20, default=0, group="Bordure"),
c_slider("border-left-width", "Épaisseur — gauche", "style:border-left-width", min=0, max=20, default=0, group="Bordure"),
]
@@ -0,0 +1,21 @@
from ..controls.c_text import c_text
# 3.1 (Confort) — interactions au survol : mécanisme générique, disponible
# sur TOUS les widgets (via UNIVERSAL_CONTROLS), pour révéler un texte
# différent quand le joueur survole l'élément avec la souris — le cas
# d'usage documenté : survoler un nom révèle l'adresse réelle, survoler un
# lien révèle l'URL réelle. Laisser vide = aucun changement au survol (donc
# aucune régression sur les jeux existants, dont l'attribut est absent).
# Rendu : voir render_element_html.py (attribut data-hover-text) et
# bindHoverTexts() dans templates/play.html (échange du texte au survol,
# ignore volontairement les éléments qui ont des enfants — conteneurs — pour
# ne jamais écraser une mise en page imbriquée).
HOVER_CONTROLS = [
c_text(
"survol_texte",
"Texte affiché au survol de la souris",
"attr:_survol_texte",
placeholder="ex. mathilde.d@forgebase.fr, https://vrai-lien.exemple...",
group="Survol",
),
]
@@ -0,0 +1,44 @@
from ..controls.c_preset import c_preset
from ..controls.c_checkbox import c_checkbox
from ..controls.c_slider import c_slider
LAYOUT_CONTROLS = [
c_preset(
"disposition_interne", "Disposition interne",
[
("libre", "Empilement libre", {"display": "", "flex-direction": ""}),
("ligne", "Alignés côte à côte", {"display": "flex", "flex-direction": "row"}),
("ligne_inverse", "Alignés côte à côte, ordre inversé", {"display": "flex", "flex-direction": "row-reverse"}),
("colonne", "Alignés les uns sous les autres", {"display": "flex", "flex-direction": "column"}),
("colonne_inverse", "Alignés les uns sous les autres, ordre inversé", {"display": "flex", "flex-direction": "column-reverse"}),
],
default="libre", group="Disposition",
),
c_preset(
"alignement_transversal", "Alignement transversal",
[
("etire", "Étirés pour remplir", {"align-items": ""}),
("debut", "Alignés au début", {"align-items": "flex-start"}),
("centre_t", "Centrés", {"align-items": "center"}),
("fin", "Alignés à la fin", {"align-items": "flex-end"}),
],
default="etire", group="Disposition",
),
c_preset(
"alignement_principal", "Répartition",
[
("debut_p", "Regroupés au début", {"justify-content": ""}),
("centre_p", "Regroupés au centre", {"justify-content": "center"}),
("fin_p", "Regroupés à la fin", {"justify-content": "flex-end"}),
("espace_entre", "Espacement égal entre eux", {"justify-content": "space-between"}),
("espace_autour", "Espacement égal autour de chacun", {"justify-content": "space-around"}),
("espace_uniforme", "Espacement totalement uniforme", {"justify-content": "space-evenly"}),
],
default="debut_p", group="Disposition",
),
c_checkbox(
"flex-wrap", "Autoriser le retour à la ligne si ça ne tient pas",
"style:flex-wrap", true_value="wrap", false_value="nowrap", default=False, group="Disposition",
),
c_slider("gap", "Espace entre les éléments", "style:gap", min=0, max=40, default=8, group="Disposition"),
]
@@ -0,0 +1,16 @@
from ..controls.c_slider import c_slider
# Taille fixe (en pixels), surtout utile pour un élément posé À L'INTÉRIEUR
# d'un conteneur/répéteur/groupe de champs : par défaut, ces éléments
# remplissent la largeur disponible et s'ajustent en hauteur à leur contenu
# (voir _style_string) — 0 garde ce comportement automatique ; une valeur
# non nulle impose une taille fixe (ex: une image de 120px de large dans une
# ligne, pour que "Espacement égal entre eux"/"autour"/"uniforme" sur le
# conteneur parent ait un effet visible, ce qui n'est pas le cas tant que
# chaque élément remplit 100% de la largeur).
SIZE_OVERRIDE_CONTROLS = [
c_slider("child-width", "Largeur fixe", "style:width",
min=0, max=1000, default=0, group="Taille dans le conteneur"),
c_slider("child-height", "Hauteur fixe", "style:height",
min=0, max=1000, default=0, group="Taille dans le conteneur"),
]
@@ -0,0 +1,20 @@
from ..controls.c_slider import c_slider
# Les réglages "par côté" sont volontairement placés APRÈS les réglages
# globaux ci-dessous dans cette même liste : le style HTML final est généré
# dans cet ordre, et pour une même propriété CSS (ex. padding-top vs
# padding), la déclaration qui apparaît EN DERNIER dans l'attribut style
# l'emporte — donc un réglage par côté prime bien sur le réglage global s'il
# est utilisé en plus.
SPACING_CONTROLS = [
c_slider("padding", "Marge intérieure — tous les côtés", "style:padding", min=0, max=60, default=0, group="Espacement"),
c_slider("margin", "Marge extérieure — tous les côtés", "style:margin", min=0, max=60, default=0, group="Espacement"),
c_slider("padding-top", "Padding — haut", "style:padding-top", min=0, max=60, default=0, group="Espacement"),
c_slider("padding-right", "Padding — droite", "style:padding-right", min=0, max=60, default=0, group="Espacement"),
c_slider("padding-bottom", "Padding — bas", "style:padding-bottom", min=0, max=60, default=0, group="Espacement"),
c_slider("padding-left", "Padding — gauche", "style:padding-left", min=0, max=60, default=0, group="Espacement"),
c_slider("margin-top", "Margin — haut", "style:margin-top", min=0, max=60, default=0, group="Espacement"),
c_slider("margin-right", "Margin — droite", "style:margin-right", min=0, max=60, default=0, group="Espacement"),
c_slider("margin-bottom", "Margin — bas", "style:margin-bottom", min=0, max=60, default=0, group="Espacement"),
c_slider("margin-left", "Margin — gauche", "style:margin-left", min=0, max=60, default=0, group="Espacement"),
]
@@ -0,0 +1,6 @@
from ..controls.c_select import c_select
from ..font_options import FONT_OPTIONS
TEXT_STYLE_CONTROLS_EXTRA = [
c_select("font-family", "Police de caractère", "style:font-family", FONT_OPTIONS, default="inherit", group="Texte"),
]
@@ -0,0 +1,8 @@
from ..controls.c_scale import c_scale
from .visibility_control import VISIBILITY_CONTROL
from .size_override_controls import SIZE_OVERRIDE_CONTROLS
from .spacing_controls import SPACING_CONTROLS
from .border_controls import BORDER_CONTROLS
from .hover_controls import HOVER_CONTROLS
UNIVERSAL_CONTROLS = [VISIBILITY_CONTROL] + SIZE_OVERRIDE_CONTROLS + SPACING_CONTROLS + BORDER_CONTROLS + [c_scale()] + HOVER_CONTROLS
@@ -0,0 +1,16 @@
from ..controls.c_preset import c_preset
VISIBILITY_CONTROL = c_preset(
"visibilite", "Visibilité",
[
# "display" n'est volontairement pas touché par "visible" / "invisible" :
# un conteneur en disposition ligne/colonne (flex) doit garder son
# display:flex quand il redevient visible — seul "masqué" doit
# explicitement forcer display:none (et gagne, car ce réglage est
# appliqué après la disposition interne lors de l'enregistrement).
("visible", "Visible normalement", {"visibility": ""}),
("masque", "Masqué — n'occupe plus de place", {"display": "none", "visibility": ""}),
("invisible", "Invisible — garde sa place", {"visibility": "hidden"}),
],
default="visible", group="Disposition",
)
+44
View File
@@ -0,0 +1,44 @@
import re
def _control_value(el, control):
"""Lit la valeur actuelle d'un réglage à partir de l'élément stocké
(content / attributs / style), avec repli sur sa valeur par défaut."""
ctype = control["type"]
style = el.get("style") or {}
if ctype == "preset":
for key, _label, props in control["options"]:
if all((style.get(k) or "") == (v or "") for k, v in props.items()):
return key
return control.get("default")
if ctype == "toggle_token":
_kind, name = control["target"].split(":", 1)
current = style.get(name, "") or ""
return control["token"] in current.split()
if ctype == "scale":
transform = style.get("transform", "") or ""
m = re.search(r"scale\(([\d.]+)\)", transform)
return float(m.group(1)) if m else control.get("default", 1)
target = control["target"]
if target == "content":
return el.get("content") or control.get("default", "")
kind, name = target.split(":", 1)
store = el.get("attributes") if kind == "attr" else style
store = store or {}
if ctype == "checkbox":
if name not in store:
return bool(control.get("default", False))
return store[name] not in (None, "", control.get("false_value"))
if ctype == "slider":
raw = store.get(name)
if raw is None:
return control.get("default", 0)
try:
return float(str(raw).replace(control.get("unit", ""), "").strip() or 0)
except ValueError:
return control.get("default", 0)
return store.get(name, control.get("default", ""))
+1
View File
@@ -0,0 +1 @@
"""Constructeurs de réglages ("controls") pour les widgets."""
+2
View File
@@ -0,0 +1,2 @@
def c_align(key, label, target, default="left", group=None):
return {"key": key, "label": label, "type": "align", "target": target, "default": default, "group": group}
+2
View File
@@ -0,0 +1,2 @@
def c_checkbox(key, label, target, true_value="1", false_value="", default=False, group=None):
return {"key": key, "label": label, "type": "checkbox", "target": target, "true_value": true_value, "false_value": false_value, "default": default, "group": group}
+2
View File
@@ -0,0 +1,2 @@
def c_color(key, label, target, default="#e8eaf0", group=None):
return {"key": key, "label": label, "type": "color", "target": target, "default": default, "group": group}
+3
View File
@@ -0,0 +1,3 @@
def c_preset(key, label, options, default=None, group=None):
"""options: liste de (clé, libellé, {propriété_css: valeur, ...})."""
return {"key": key, "label": label, "type": "preset", "options": options, "default": default or options[0][0], "group": group}
+2
View File
@@ -0,0 +1,2 @@
def c_scale(key="scale", label="Échelle", group="Disposition"):
return {"key": key, "label": label, "type": "scale", "min": 0.1, "max": 3, "step": 0.05, "default": 1, "group": group}
+2
View File
@@ -0,0 +1,2 @@
def c_select(key, label, target, options, default=None, group=None):
return {"key": key, "label": label, "type": "select", "target": target, "options": options, "default": default or options[0][0], "group": group}
+2
View File
@@ -0,0 +1,2 @@
def c_slider(key, label, target, unit="px", min=0, max=100, step=1, default=16, group=None):
return {"key": key, "label": label, "type": "slider", "target": target, "unit": unit, "min": min, "max": max, "step": step, "default": default, "group": group}
+2
View File
@@ -0,0 +1,2 @@
def c_text(key, label, target, placeholder="", default="", group=None):
return {"key": key, "label": label, "type": "text", "target": target, "placeholder": placeholder, "default": default, "group": group}
+2
View File
@@ -0,0 +1,2 @@
def c_textarea(key, label, target, placeholder="", default="", group=None):
return {"key": key, "label": label, "type": "textarea", "target": target, "placeholder": placeholder, "default": default, "group": group}
@@ -0,0 +1,2 @@
def c_toggle_token(key, label, target, token, group=None):
return {"key": key, "label": label, "type": "toggle_token", "target": target, "token": token, "group": group}
+3
View File
@@ -0,0 +1,3 @@
def c_url(key, label, target, placeholder="https://...", default="", allow_upload=False, upload_accept="*/*", group=None):
return {"key": key, "label": label, "type": "url", "target": target, "placeholder": placeholder, "default": default,
"allow_upload": allow_upload, "upload_accept": upload_accept, "group": group}
+24
View File
@@ -0,0 +1,24 @@
import db
from .widget_meta import widget_meta
from .control_value import _control_value
from ..element_types.list_element_types import list_element_types
def controls_with_values(slug, el):
"""Pour le panneau de propriétés : la liste des réglages du widget de
cet élément, chacun avec sa valeur actuelle déjà résolue. Le réglage
"définition_id" du Répéteur de données a besoin du jeu courant pour
lister les objets disponibles — d'où le paramètre slug."""
meta = widget_meta(el.get("widget"))
result = []
for control in meta.get("controls", []):
control = dict(control)
if control["key"] == "definition_id":
defs = db.list_definitions(slug)
control["options"] = [("", "(sélectionner un objet)")] + [(str(d["id"]), d["name"]) for d in defs]
if control["key"] == "template_type_id":
types = list_element_types(slug)
control["options"] = [("", "(aucun — utiliser le modèle de texte ci-dessous)")] + [(str(t["id"]), t["name"]) for t in types]
result.append({**control, "value": _control_value(el, control)})
return result
@@ -0,0 +1,75 @@
from .widget_meta import widget_meta
# Ces deux réglages utilisent 0 comme valeur sentinelle "automatique" (voir
# size_override_controls.py / _style_string) : contrairement aux autres
# sliders, 0 n'y est PAS une vraie valeur par défaut à figer, donc on ne les
# fige jamais à la création — les autres sliders à 0 par défaut (ex:
# épaisseur de bordure) doivent au contraire être figés explicitement, sinon
# un border-style baké sans border-width associé afficherait une bordure par
# défaut du navigateur (~3px, "medium") au lieu de rester invisible.
_AUTO_SIZE_KEYS = {"child-width", "child-height"}
# Réglages "par côté" (padding/margin/bordure individuels) : contrairement
# aux réglages "tous les côtés" équivalents (qui, eux, DOIVENT être figés à
# la création — voir plus bas), ceux-ci ne doivent jamais l'être. Une fois
# figés (donc présents dans le style stocké), save_element_controls.py les
# réécrirait à CHAQUE enregistrement du panneau (puisqu'il envoie tous les
# réglages en un seul formulaire) — et comme ils sont placés APRÈS le
# réglage global correspondant dans les listes de contrôles (pour pouvoir le
# surcharger quand on s'en sert), une valeur par défaut ainsi figée à 0
# écraserait alors silencieusement tout changement fait sur le réglage
# global (ex: "Épaisseur de la bordure" à 5px se retrouverait avec un côté
# resté à 0px). Ils doivent donc rester ABSENTS du style tant qu'on ne les a
# pas vraiment personnalisés.
_PER_SIDE_KEYS = {
"padding-top", "padding-right", "padding-bottom", "padding-left",
"margin-top", "margin-right", "margin-bottom", "margin-left",
"border-top-width", "border-right-width", "border-bottom-width", "border-left-width",
}
def default_style_and_attributes(widget_key):
"""Calcule le style/attributs de DÉPART d'un élément fraîchement posé sur
l'écran, pour les réglages dont la valeur par défaut a un effet visuel
voulu dès la création (ex: "Ajustement dans son cadre" = cover pour une
image, afin qu'elle se recadre proprement sans être écrasée) plutôt que
de rester une simple valeur d'aperçu dans le panneau de propriétés.
Les couleurs (fond, bordure) sont volontairement EXCLUES : leur valeur
"par défaut" n'est qu'une suggestion affichée dans le sélecteur de
couleur, pas un vrai réglage neutre (le neutre CSS est transparent) — la
figer dès la création ferait apparaître une couleur que personne n'a
demandée. Voir save_element_controls.py pour la même règle appliquée
aux modifications ultérieures via le panneau de propriétés."""
meta = widget_meta(widget_key)
attributes, style = {}, {}
for control in meta.get("controls", []):
ctype = control["type"]
if ctype in ("checkbox", "toggle_token", "preset", "scale", "color"):
continue
target = control.get("target")
if not target or target == "content":
continue
default = control.get("default", "")
if default in (None, ""):
continue
if ctype == "slider":
if control["key"] in _AUTO_SIZE_KEYS and str(default) in ("0",):
continue # 0 = comportement automatique, rien à figer
if control["key"] in _PER_SIDE_KEYS:
continue # réglage par côté : jamais figé (voir commentaire plus haut)
unit = control.get("unit", "")
default = f"{default}{unit}"
kind, name = target.split(":", 1)
(attributes if kind == "attr" else style)[name] = default
if widget_key == "superposition":
# 3.4 (Confort) — overlay/modale réutilisable : contrairement aux
# autres widgets, une Superposition démarre MASQUÉE par défaut dès
# sa création (position:fixed; inset:0 la ferait sinon couvrir tout
# l'écran immédiatement, avant même qu'une action l'affiche). Le
# créateur du jeu doit poser une action "Modifier un élément →
# Visibilité → Rendre visible" pour l'ouvrir — cohérent avec
# "fermeture manuelle uniquement".
style["display"] = "none"
return attributes, style
+15
View File
@@ -0,0 +1,15 @@
FONT_OPTIONS = [
("inherit", "Police par défaut"),
("Georgia, serif", "Georgia (classique, avec empattements)"),
("'Courier New', monospace", "Courier (machine à écrire)"),
("'Trebuchet MS', sans-serif", "Trebuchet (arrondie)"),
("'Poppins', sans-serif", "Poppins (Google Fonts, moderne)"),
("'Roboto', sans-serif", "Roboto (Google Fonts, neutre)"),
("'Montserrat', sans-serif", "Montserrat (Google Fonts, affirmée)"),
("'Playfair Display', serif", "Playfair Display (Google Fonts, élégante)"),
]
GOOGLE_FONTS_LINK = (
"https://fonts.googleapis.com/css2?"
"family=Poppins:wght@400;600;700&family=Roboto:wght@400;700&"
"family=Montserrat:wght@400;700&family=Playfair+Display:wght@400;700&display=swap"
)
@@ -0,0 +1,8 @@
# "superposition" (3.4, overlay/modale) est volontairement ABSENT d'ici :
# son style "display" est piloté à 100% par render_overlay.py (position
# fixe + display:flex codés en dur pour le voile, quelle que soit la
# disposition) — lui donner le contrôle "Disposition interne" (LAYOUT_
# CONTROLS ci-dessous) écraserait le display:none posé par défaut à sa
# création (voir default_style_for_widget.py) dès le premier enregistrement
# du panneau de propriétés, rendant la modale visible en permanence.
LAYOUT_CAPABLE_WIDGETS = {"conteneur", "repeteur_donnees", "liste", "liste_numerotee", "groupe_champs", "tableau"}
+261
View File
@@ -0,0 +1,261 @@
from .controls.c_text import c_text
from .controls.c_textarea import c_textarea
from .controls.c_url import c_url
from .controls.c_color import c_color
from .controls.c_slider import c_slider
from .controls.c_checkbox import c_checkbox
from .controls.c_toggle_token import c_toggle_token
from .controls.c_align import c_align
from .controls.c_select import c_select
from .control_groups.text_style_controls_extra import TEXT_STYLE_CONTROLS_EXTRA
from ..flow.constants import CONDITION_OPERATORS
WIDGETS = {
"texte": {
"label": "Texte", "icon": "📝", "tag": "p", "has_content": True,
"w": 40, "h": 10, "default_content": "Texte du paragraphe.",
"controls": [
c_textarea("content", "Texte", "content", default="Texte du paragraphe.", group="Contenu"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=64, default=16, group="Texte"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_align("text-align", "Alignement", "style:text-align", group="Texte"),
c_checkbox("bold", "Gras", "style:font-weight", "bold", "normal", group="Texte"),
c_checkbox("italic", "Italique", "style:font-style", "italic", "normal", group="Texte"),
c_toggle_token("underline", "Souligné", "style:text-decoration", "underline", group="Texte"),
c_toggle_token("strikethrough", "Barré", "style:text-decoration", "line-through", group="Texte"),
] + TEXT_STYLE_CONTROLS_EXTRA,
},
"titre": {
"label": "Titre", "icon": "🔠", "tag": None, "has_content": True,
"w": 55, "h": 10, "default_content": "Titre",
"controls": [
c_textarea("content", "Texte", "content", default="Titre", group="Contenu"),
c_select("niveau", "Importance du titre", "attr:_niveau",
[("1", "Très grand — titre principal"), ("2", "Grand"), ("3", "Moyen")], default="2", group="Contenu"),
c_slider("font-size", "Taille du texte", "style:font-size", min=14, max=80, default=32, group="Texte"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_align("text-align", "Alignement", "style:text-align", default="left", group="Texte"),
c_checkbox("italic", "Italique", "style:font-style", "italic", "normal", group="Texte"),
c_toggle_token("underline", "Souligné", "style:text-decoration", "underline", group="Texte"),
] + TEXT_STYLE_CONTROLS_EXTRA,
},
"bouton": {
"label": "Bouton", "icon": "🔘", "tag": "button", "has_content": True,
"w": 30, "h": 8, "default_content": "Valider",
"controls": [
c_text("content", "Texte du bouton", "content", default="Valider", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#ffffff", group="Texte"),
c_color("background", "Couleur de fond", "style:background-color", "#5b8cff", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=15, group="Texte"),
c_slider("border-radius", "Arrondi des coins", "style:border-radius", min=0, max=40, default=8, group="Texte"),
c_checkbox("submit", "Valide un formulaire", "attr:type", "submit", "button", group="Contenu"),
] + TEXT_STYLE_CONTROLS_EXTRA,
},
"lien": {
"label": "Lien", "icon": "🔗", "tag": "a", "has_content": True,
"w": 25, "h": 6, "default_content": "Cliquez ici",
"controls": [
c_text("content", "Texte du lien", "content", default="Cliquez ici", group="Contenu"),
c_url("href", "Destination", "attr:href", default="#", group="Contenu"),
c_checkbox("new_tab", "Ouvrir dans un nouvel onglet", "attr:target", "_blank", "", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#5b8cff", group="Texte"),
c_toggle_token("underline", "Souligné", "style:text-decoration", "underline", group="Texte"),
] + TEXT_STYLE_CONTROLS_EXTRA,
},
"image": {
"label": "Image", "icon": "🖼️", "tag": "img", "has_content": False, "self_closing": True,
"w": 35, "h": 22, "default_content": "",
"controls": [
c_url("src", "Source de l'image", "attr:src", allow_upload=True, upload_accept="image/*", group="Contenu"),
c_text("alt", "Texte alternatif", "attr:alt", placeholder="décrit l'image pour l'accessibilité", group="Contenu"),
c_select("object-fit", "Ajustement dans son cadre", "style:object-fit",
[("cover", "Remplir en recadrant"), ("contain", "Tout montrer sans déformer"), ("fill", "Étirer pour remplir tout le cadre")],
default="cover", group="Média"),
c_slider("border-radius", "Arrondi des coins", "style:border-radius", min=0, max=60, default=0, group="Média"),
],
},
"video": {
"label": "Vidéo", "icon": "🎬", "tag": "video", "has_content": False,
"w": 40, "h": 25, "default_content": "",
"controls": [
c_url("src", "Source de la vidéo", "attr:src", allow_upload=True, upload_accept="video/*", group="Contenu"),
c_checkbox("controls", "Afficher les contrôles de lecture", "attr:controls", "controls", "", default=True, group="Contenu"),
c_checkbox("autoplay", "Lecture automatique", "attr:autoplay", "autoplay", "", group="Contenu"),
c_checkbox("loop", "Lecture en boucle", "attr:loop", "loop", "", group="Contenu"),
],
},
"champ_texte": {
"label": "Champ — texte", "icon": "⌨️", "tag": "input", "has_content": False, "self_closing": True,
"w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "text"},
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Votre réponse", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. reponse1", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
],
},
"champ_email": {
"label": "Champ — email", "icon": "📧", "tag": "input", "has_content": False, "self_closing": True,
"w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "email"},
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. votre@email.com", default="votre@email.com", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. email", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
],
},
"champ_mot_de_passe": {
"label": "Champ — mot de passe", "icon": "🔒", "tag": "input", "has_content": False, "self_closing": True,
"w": 40, "h": 8, "default_content": "", "fixed_attrs": {"type": "password"},
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Mot de passe", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. mot_de_passe", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
],
},
"case_a_cocher": {
"label": "Case à cocher", "icon": "☑️", "tag": "input", "has_content": True, "special_render": "checkbox_label",
"w": 35, "h": 8, "default_content": "Option",
"controls": [
c_text("content", "Texte affiché", "content", default="Option", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. accepte_cgu", group="Contenu"),
c_text("value", "Valeur envoyée", "attr:value", placeholder="ex. oui", group="Contenu"),
c_checkbox("checked", "Coché par défaut", "attr:checked", "checked", "", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
],
},
"bouton_radio": {
"label": "Bouton radio", "icon": "🔘", "tag": "input", "has_content": True, "special_render": "checkbox_label",
"w": 35, "h": 8, "default_content": "Option",
"controls": [
c_text("content", "Texte affiché", "content", default="Option", group="Contenu"),
c_text("name", "Nom du groupe", "attr:name", placeholder="ex. choix1", group="Contenu"),
c_text("value", "Valeur envoyée", "attr:value", placeholder="ex. optionA", group="Contenu"),
c_checkbox("checked", "Sélectionné par défaut", "attr:checked", "checked", "", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
],
},
"zone_texte": {
"label": "Zone de texte", "icon": "🗒️", "tag": "textarea", "has_content": False,
"w": 40, "h": 18, "default_content": "",
"controls": [
c_text("placeholder", "Texte indicatif", "attr:placeholder", placeholder="ex. Votre message", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. message", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
],
},
"liste_deroulante": {
"label": "Liste déroulante", "icon": "🔽", "tag": "select", "has_content": True, "special_render": "select_options",
"w": 35, "h": 8, "default_content": "Option 1\nOption 2\nOption 3",
"controls": [
c_textarea("content", "Options — une par ligne", "content", default="Option 1\nOption 2\nOption 3", group="Contenu"),
c_text("name", "Nom du champ", "attr:name", placeholder="ex. choix", group="Contenu"),
c_checkbox("required", "Obligatoire", "attr:required", "required", "", group="Contenu"),
],
},
"groupe_champs": {
"label": "Groupe de champs", "icon": "🗃️", "tag": "fieldset", "has_content": True, "special_render": "fieldset_legend",
"w": 55, "h": 30, "default_content": "Groupe de champs",
"controls": [
c_text("content", "Titre du groupe", "content", default="Groupe de champs", group="Contenu"),
],
},
"liste": {
"label": "Liste à puces", "icon": "📋", "tag": "ul", "has_content": True,
"w": 40, "h": 22, "default_content": "Premier élément\nDeuxième élément\nTroisième élément",
"controls": [
c_textarea("content", "Éléments de la liste — un par ligne", "content",
default="Premier élément\nDeuxième élément\nTroisième élément", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=16, group="Texte"),
],
},
"liste_numerotee": {
"label": "Liste numérotée", "icon": "🔢", "tag": "ol", "has_content": True,
"w": 40, "h": 22, "default_content": "Premier élément\nDeuxième élément\nTroisième élément",
"controls": [
c_textarea("content", "Éléments de la liste — un par ligne", "content",
default="Premier élément\nDeuxième élément\nTroisième élément", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=16, group="Texte"),
],
},
"tableau": {
"label": "Tableau", "icon": "🗂️", "tag": "table", "has_content": True, "special_render": "table",
"w": 55, "h": 25, "default_content": "Colonne 1;Colonne 2\nValeur A;Valeur B",
"controls": [
c_textarea("content", "Contenu — une ligne par rangée, colonnes séparées par ;", "content",
default="Colonne 1;Colonne 2\nValeur A;Valeur B", group="Contenu"),
c_checkbox("header_row", "Première ligne = en-tête", "attr:_header_row", "1", "", default=True, group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
],
},
"conteneur": {
"label": "Conteneur / décor", "icon": "⬛", "tag": "div", "has_content": True,
"w": 50, "h": 30, "default_content": "",
"controls": [
c_text("content", "Texte", "content", group="Contenu"),
c_color("background", "Couleur de fond", "style:background-color", "#1f2430", group="Texte"),
c_slider("border-radius", "Arrondi des coins", "style:border-radius", min=0, max=60, default=10, group="Texte"),
c_slider("opacity", "Opacité", "style:opacity", unit="", min=0, max=1, step=0.05, default=1, group="Texte"),
],
},
"separateur": {
"label": "Séparateur", "icon": "➖", "tag": "hr", "has_content": False, "self_closing": True,
"w": 50, "h": 2, "default_content": "",
"controls": [
c_color("color", "Couleur", "style:border-color", "#2b3140", group="Texte"),
],
},
"repeteur_donnees": {
"label": "Répéteur de données", "icon": "🔁", "tag": "div", "has_content": True, "special_render": "repeater",
"w": 55, "h": 35, "default_content": "{{nom}}",
"controls": [
c_select("definition_id", "Objet de données à afficher", "attr:_definition_id",
[("", "Sélectionner un objet")], default="", group="Contenu"),
c_select("template_type_id", "Modèle de ligne — élément de jeu", "attr:_template_type_id",
[("", "Aucun — utiliser le modèle de texte ci-dessous")], default="", group="Contenu"),
c_textarea("content", "Modèle d'affichage texte — utilise {{nom_du_champ}}", "content", default="{{nom}}", group="Contenu"),
c_color("color", "Couleur du texte", "style:color", "#e8eaf0", group="Texte"),
c_slider("font-size", "Taille du texte", "style:font-size", min=10, max=40, default=16, group="Texte"),
c_text("filtre_champ", "Filtrer : n'afficher que les lignes où ce champ",
"attr:_filtre_champ", placeholder="ex. niveau", default="", group="Filtre"),
c_select("filtre_operateur", "...est", "attr:_filtre_operateur", CONDITION_OPERATORS, default="egal", group="Filtre"),
c_text("filtre_valeur", "...cette valeur, ou {{NomObjet.champ}} pour comparer à une donnée qui change en cours de partie",
"attr:_filtre_valeur", placeholder="ex. 3 ou {{Partie.niveau_courant}}", default="", group="Filtre"),
],
},
"jauge": {
"label": "Jauge — liée à une donnée", "icon": "📊", "tag": "div", "has_content": False, "special_render": "jauge",
"w": 60, "h": 12, "default_content": "",
"controls": [
# La clé "definition_id" est reconnue spécifiquement dans
# controls_with_values.py pour être peuplée avec les VRAIS
# objets de données du jeu — même mécanisme que le Répéteur.
c_select("definition_id", "Objet de données à suivre", "attr:_definition_id",
[("", "Sélectionner un objet")], default="", group="Donnée"),
c_text("champ", "Champ numérique à afficher",
"attr:_champ", placeholder="ex. reputation", default="", group="Donnée"),
c_text("min", "Valeur mini", "attr:_min", default="0", group="Donnée"),
c_text("max", "Valeur maxi", "attr:_max", default="100", group="Donnée"),
c_color("couleur_basse", "Couleur côté mini", "attr:_couleur_basse", "#e05252", group="Apparence"),
c_color("couleur_haute", "Couleur côté maxi", "attr:_couleur_haute", "#4caf50", group="Apparence"),
c_checkbox("afficher_valeur", "Afficher la valeur en chiffres sur la jauge", "attr:_afficher_valeur", "1", "", default=False, group="Apparence"),
],
},
"superposition": {
# 3.4 (Confort) — overlay/modale réutilisable : couvre TOUJOURS tout
# l'écran (position:fixed, indépendant de x/y/width/height — voir
# render_overlay.py), avec un voile semi-transparent + une boîte
# centrée qui contient les éléments posés à l'intérieur. Fermeture
# manuelle uniquement : réutilise l'action existante "Modifier un
# élément → Visibilité" (masquer/rendre visible), comme n'importe
# quel autre élément — pas de mécanisme de fermeture au clic externe.
"label": "Superposition / boîte de dialogue", "icon": "🪟", "tag": "div", "has_content": True,
"special_render": "overlay",
"w": 50, "h": 40, "default_content": "",
"controls": [
c_text("content", "Texte — en plus des éléments posés dans la boîte", "content", group="Contenu"),
c_color("couleur_boite", "Couleur de fond de la boîte", "attr:_couleur_boite", "#1f2430", group="Apparence"),
c_slider("arrondi", "Arrondi des coins de la boîte", "attr:_arrondi", unit="", min=0, max=40, default=12, group="Apparence"),
],
},
}
+14
View File
@@ -0,0 +1,14 @@
from .registry import WIDGETS
from .layout_capable_widgets import LAYOUT_CAPABLE_WIDGETS
from .control_groups.layout_controls import LAYOUT_CONTROLS
from .control_groups.universal_controls import UNIVERSAL_CONTROLS
def widget_meta(widget_key):
base = dict(WIDGETS.get(widget_key, WIDGETS["conteneur"]))
controls = list(base.get("controls", []))
if widget_key in LAYOUT_CAPABLE_WIDGETS:
controls = controls + LAYOUT_CONTROLS
controls = controls + UNIVERSAL_CONTROLS
base["controls"] = controls
return base