first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Catalogue de widgets et constructeurs de réglages ("controls")."""
|
||||
@@ -0,0 +1 @@
|
||||
BORDER_STYLE_OPTIONS = [("solid", "Trait plein"), ("dashed", "Tirets"), ("dotted", "Pointillés"), ("none", "Aucune")]
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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", ""))
|
||||
@@ -0,0 +1 @@
|
||||
"""Constructeurs de réglages ("controls") pour les widgets."""
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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"),
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user