Ajoute une propriété "Ombre portée" (box-shadow), disponible sur tout widget

Nouveau groupe "Ombre" dans le panneau de propriétés, à côté de "Bordure"
(même universalité — voir UNIVERSAL_CONTROLS) : décalage X/Y, flou,
étendue, couleur et opacité, combinés en une seule valeur CSS box-shadow
(couleur+opacité fusionnées en rgba(), un <input type="color"> seul ne
portant pas de canal alpha). Décalages/flou/étendue tous à 0 = pas
d'ombre, même convention que border-width à 0 = pas de bordure.

Nouveau ctype "shadow" (c_shadow.py), suit exactement le même principe
que le ctype "size" déjà existant (size_override_controls.py) : plusieurs
entrées de formulaire pour un seul réglage, composées/décomposées dans
save_element_controls.py et control_value.py plutôt que passées par le
chemin générique clé->valeur.

Ajoute tests/test_shadow_controls.py (présence dans le panneau,
composition de la valeur CSS, aller-retour dans le formulaire, remise à
zéro qui retire l'ombre).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-08-25 14:37:38 +02:00
co-authored by Claude Sonnet 5
parent 9c9e3c5379
commit bda082bffd
7 changed files with 151 additions and 1 deletions
+41
View File
@@ -6,6 +6,16 @@ from ..widgets.widget_meta import widget_meta
from .get_element import get_element
def _hex_to_rgb(hex_color):
h = (hex_color or "").lstrip("#")
if len(h) != 6:
return (0, 0, 0)
try:
return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
except ValueError:
return (0, 0, 0)
def _is_default_value(control, raw_value):
"""Compare la valeur reçue du formulaire à la valeur par défaut du
réglage (en tenant compte de l'unité ajoutée pour les sliders), pour
@@ -83,6 +93,37 @@ def save_element_controls(slug, element_id, form):
style[name] = f"{num_str}{unit}"
continue
if ctype == "shadow":
# 6 entrées de formulaire (décalage X/Y, flou, étendue, couleur,
# opacité — voir c_shadow.py) combinées en une seule valeur CSS
# "box-shadow" (couleur+opacité fusionnées en rgba(), un
# <input type="color"> seul ne portant pas de canal alpha).
_kind, name = control["target"].split(":", 1)
defaults = control.get("default", {})
def _num(suffix, fallback):
try:
return float(form.get(field_name + suffix, fallback))
except (TypeError, ValueError):
return fallback
x = _num("_x", defaults.get("x", 0))
y = _num("_y", defaults.get("y", 4))
blur = _num("_blur", defaults.get("blur", 12))
spread = _num("_spread", defaults.get("spread", 0))
opacity = _num("_opacity", defaults.get("opacity", 0.4))
color = form.get(field_name + "_color") or defaults.get("color", "#000000")
if x == 0 and y == 0 and blur == 0 and spread == 0:
style.pop(name, None)
else:
r, g, b = _hex_to_rgb(color)
def _px(n):
return str(int(n)) if n == int(n) else str(n)
style[name] = f"{_px(x)}px {_px(y)}px {_px(blur)}px {_px(spread)}px rgba({r}, {g}, {b}, {opacity})"
continue
target = control["target"]
if ctype == "checkbox":
checked = form.get(field_name) is not None
@@ -0,0 +1,5 @@
from ..controls.c_shadow import c_shadow
SHADOW_CONTROLS = [
c_shadow("box-shadow", "Ombre portée", "style:box-shadow", group="Ombre"),
]
@@ -4,6 +4,7 @@ from .visibility_condition_controls import VISIBILITY_CONDITION_CONTROLS
from .size_override_controls import SIZE_OVERRIDE_CONTROLS
from .spacing_controls import SPACING_CONTROLS
from .border_controls import BORDER_CONTROLS
from .shadow_controls import SHADOW_CONTROLS
# Le panneau "Survol" (HOVER_CONTROLS) a été retiré du panneau de propriétés :
# survoler un élément est conceptuellement un DÉCLENCHEUR (voir la Logique de
@@ -12,4 +13,4 @@ from .border_controls import BORDER_CONTROLS
# et bindHoverTexts() dans play.html) reste fonctionnel pour les éléments qui
# en avaient déjà un, seul le moyen de le RÉGLER depuis ce panneau disparaît
# pour l'instant.
UNIVERSAL_CONTROLS = [VISIBILITY_CONTROL, c_scale()] + SIZE_OVERRIDE_CONTROLS + SPACING_CONTROLS + BORDER_CONTROLS + VISIBILITY_CONDITION_CONTROLS
UNIVERSAL_CONTROLS = [VISIBILITY_CONTROL, c_scale()] + SIZE_OVERRIDE_CONTROLS + SPACING_CONTROLS + BORDER_CONTROLS + SHADOW_CONTROLS + VISIBILITY_CONDITION_CONTROLS
+15
View File
@@ -31,6 +31,21 @@ def _control_value(el, control):
return {"value": float(m.group(1)), "unit": m.group(2)}
return {"value": control.get("default", 0), "unit": "%"}
if ctype == "shadow":
_kind, name = control["target"].split(":", 1)
raw = style.get(name)
m = re.match(
r"^(-?\d+)px (-?\d+)px (\d+)px (\d+)px rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)$",
str(raw or ""),
)
if m:
x, y, blur, spread, r, g, b, a = m.groups()
return {
"x": int(x), "y": int(y), "blur": int(blur), "spread": int(spread),
"color": "#{:02x}{:02x}{:02x}".format(int(r), int(g), int(b)), "opacity": float(a),
}
return control.get("default", {})
target = control["target"]
if target == "content":
return el.get("content") or control.get("default", "")
+12
View File
@@ -0,0 +1,12 @@
def c_shadow(key, label, target, default=None, group=None):
"""Ombre portée composée de 6 sous-réglages (décalage X/Y, flou,
étendue, couleur, opacité) — voir save_element_controls.py (ctype
"shadow") pour comment ils se combinent en une seule valeur CSS
"box-shadow", et control_value.py pour comment elle se décompose à
nouveau pour l'affichage. Décalages/flou/étendue tous à 0 = pas
d'ombre, comme border-width à 0 = pas de bordure."""
return {
"key": key, "label": label, "type": "shadow", "target": target,
"default": default or {"x": 0, "y": 4, "blur": 12, "spread": 0, "color": "#000000", "opacity": 0.4},
"group": group,
}