Override CSS scopé aux classes .btn-* (jamais les variables --bs-* racine,
donc alertes/badges inchangés) pour remplacer les couleurs dérivées du thème
Simplex (danger mauve, secondary gris illisible). Trois rôles : la couleur
encode la fonction, le remplissage l'emphase.
- primary terracotta : action principale validante
- secondary gris ardoise : action neutre/alternative (lisible)
- danger rouge #c0392b : destructif (outline par défaut, plein en confirmation)
Audit + conformation des usages :
- compte_admin « Supprimer mon compte » : primary outline -> danger outline
- compte_abonnement « Me désabonner » : outline-primary -> outline-danger
- compte_abonnement « Je suis sûr » : outline-primary -> danger plein
Tests : ajout tests/test_boutons.py (garde de non-régression du primary).
conftest : implémente le hook pytest_setup_options (--headless=new) ; la
fixture chrome_options n'était jamais utilisée par dash.testing, d'où les
fenêtres Chrome qui s'ouvraient pendant les tests.
Specs et plan : docs/superpowers/{specs,plans}/2026-06-30-charte-boutons*.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 KiB
Charte graphique des boutons — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Remplacer les couleurs de boutons dérivées du thème Simplex (danger mauve, secondary gris illisible) par une charte à trois rôles — primary terracotta, secondary gris ardoise, danger rouge — où la couleur encode la fonction et le remplissage l'emphase.
Architecture: Override CSS ciblant exclusivement les classes .btn-* dans src/assets/css/style.css (jamais les variables --bs-* racine, pour ne pas affecter alertes/badges). Le code applicatif continue d'utiliser color="primary|secondary|danger" + outline=True|False natifs de dash-bootstrap-components. Puis audit des boutons existants pour conformer leur usage à la charte.
Tech Stack: Dash 3.4, dash-bootstrap-components, Bootstrap 5 (thème Simplex), CSS, pytest + Selenium (DashComposite).
Global Constraints
- Override uniquement les sélecteurs
.btn-*; ne jamais modifier les variables racine--bs-primary,--bs-danger,--bs-secondary(alertes/badges doivent rester inchangés). Source : spec « Hors périmètre ». - Imports applicatifs toujours préfixés
src.(ex.src.pages.recherche). - Palette exacte : terracotta
#b33821, danger rouge#c0392b, secondary texte#344054/ bord#5a6570, disabled bord#ccc/ texte#666. border-radius: 3pxet typographieInterpoids 400 conservés sur les boutons.- Focus clavier visible (
:focus-visible) sur tous les boutons restylés (accessibilité). - Respecter
prefers-reduced-motionpour toute transition de survol. - Tests :
rtk pytest(Selenium, nécessite Chrome/Chromium).DEVELOPMENT=trueest positionné automatiquement.
Task 1: Override CSS des trois rôles de boutons
Redéfinit l'apparence des classes .btn-primary, .btn-outline-primary, .btn-secondary, .btn-outline-secondary, .btn-danger, .btn-outline-danger dans la feuille de style applicative, en conservant l'API dbc native. Ajoute un test Selenium de non-régression vérifiant que la feuille est bien appliquée au bouton primaire de la page d'accueil (publique).
Files:
- Modify:
src/assets/css/style.css(bloc « Base Button Styles » autour des lignes 42-79) - Test:
tests/test_boutons.py(créer)
Interfaces:
-
Consumes: rien (première tâche).
-
Produces: classes CSS restylées
.btn-primary,.btn-outline-primary,.btn-secondary,.btn-outline-secondary,.btn-danger,.btn-outline-danger. Aucun symbole Python. -
Step 1: Écrire le test de non-régression (échoue d'abord car couleur cible non garantie)
Créer tests/test_boutons.py. Le bouton « Rechercher » de la page d'accueil (/, public, className="btn btn-primary" dans src/pages/recherche.py:56) doit être rendu terracotta plein (le dégradé existant a rgb(179, 56, 33) en couleur médiane → on vérifie une composante rouge dominante et un fond non transparent).
from dash.testing.composite import DashComposite
def _rgb_tuple(css_color: str) -> tuple[int, int, int]:
"""Parse 'rgb(r, g, b)' ou 'rgba(r, g, b, a)' en (r, g, b)."""
inner = css_color[css_color.index("(") + 1 : css_color.index(")")]
parts = [p.strip() for p in inner.split(",")]
return (int(parts[0]), int(parts[1]), int(parts[2]))
def test_btn_primary_is_terracotta(dash_duo: DashComposite):
from src.app import app
dash_duo.start_server(app)
dash_duo.wait_for_element("a.btn.btn-primary, button.btn.btn-primary", timeout=10)
btn = dash_duo.find_element("a.btn.btn-primary, button.btn.btn-primary")
# Le dégradé terracotta est posé via background-image ; la couleur de
# repli background-color ne doit pas être le bleu Bootstrap par défaut.
bg_image = btn.value_of_css_property("background-image")
color = btn.value_of_css_property("color")
assert "gradient" in bg_image # dégradé terracotta appliqué
r, g, b = _rgb_tuple(color)
assert (r, g, b) == (255, 255, 255) # texte blanc
- Step 2: Lancer le test pour vérifier l'état initial
Run: rtk pytest tests/test_boutons.py::test_btn_primary_is_terracotta -v
Expected: PASS si le .btn-primary actuel s'applique déjà (dégradé + texte blanc déjà présents lignes 51-65). Ce test sert de garde de non-régression : il doit rester vert après le refactor CSS. S'il échoue ici, c'est que la feuille n'est pas chargée → investiguer avant de continuer.
- Step 3: Réécrire le bloc boutons dans
style.css
Remplacer le bloc commenté/partiel « Base Button Styles » (lignes ~42-79 : le commentaire mort, button.btn.btn-primary, button.show-hide { … }, son :hover, et button[disabled]) par la charte complète ci-dessous. Conserver le dégradé terracotta existant pour .btn-primary et le button.show-hide (qui réutilise ce style).
/* ==========================================================================
Boutons — charte à 3 rôles (couleur = fonction, remplissage = emphase)
Voir docs/superpowers/specs/2026-06-30-charte-boutons-design.md
Override scopé aux classes .btn-* uniquement (n'affecte pas les alertes).
========================================================================== */
:root {
--btn-terracotta: #b33821;
--btn-terracotta-text: #fff;
--btn-secondary-text: #344054;
--btn-secondary-border: #5a6570;
--btn-secondary-hover-bg: #f1f3f5;
--btn-danger: #c0392b;
--btn-danger-text: #fff;
}
/* Base commune : rayon + typo */
.btn {
border-radius: 3px;
font-family: "Inter", sans-serif;
font-weight: 400;
}
/* --- PRIMARY plein : action principale validante (1 par contexte) --- */
button.btn.btn-primary,
a.btn.btn-primary,
button.show-hide {
display: block;
outline: 0;
color: var(--btn-terracotta-text);
border: 0;
background-image: linear-gradient(
rgb(209, 96, 73),
rgb(179, 56, 33) 26%,
rgb(159, 36, 22)
);
}
button.btn.btn-primary:hover,
a.btn.btn-primary:hover,
button.show-hide:hover {
color: var(--btn-terracotta-text);
background-image: linear-gradient(
rgb(239, 126, 103),
rgb(209, 86, 63) 26%,
rgb(189, 66, 52)
);
}
/* --- PRIMARY outline : action affirmative de moindre emphase --- */
.btn.btn-outline-primary {
color: var(--btn-terracotta);
border: 1px solid var(--btn-terracotta);
background-color: transparent;
background-image: none;
}
.btn.btn-outline-primary:hover,
.btn.btn-outline-primary:focus-visible {
color: var(--btn-terracotta-text);
background-color: var(--btn-terracotta);
border-color: var(--btn-terracotta);
}
/* --- SECONDARY : action neutre / alternative (gris ardoise) --- */
.btn.btn-secondary,
.btn.btn-outline-secondary {
color: var(--btn-secondary-text);
border: 1px solid var(--btn-secondary-border);
background-color: transparent;
background-image: none;
}
.btn.btn-secondary:hover,
.btn.btn-secondary:focus-visible,
.btn.btn-outline-secondary:hover,
.btn.btn-outline-secondary:focus-visible {
color: var(--btn-secondary-text);
background-color: var(--btn-secondary-hover-bg);
border-color: var(--btn-secondary-border);
}
/* --- DANGER outline : action destructive dans le flux courant --- */
.btn.btn-outline-danger {
color: var(--btn-danger);
border: 1px solid var(--btn-danger);
background-color: transparent;
background-image: none;
}
.btn.btn-outline-danger:hover,
.btn.btn-outline-danger:focus-visible {
color: var(--btn-danger-text);
background-color: var(--btn-danger);
border-color: var(--btn-danger);
}
/* --- DANGER plein : confirmation finale destructive (modale) --- */
.btn.btn-danger {
color: var(--btn-danger-text);
border: 1px solid var(--btn-danger);
background-color: var(--btn-danger);
background-image: none;
}
.btn.btn-danger:hover,
.btn.btn-danger:focus-visible {
color: var(--btn-danger-text);
background-color: #a93226;
border-color: #a93226;
}
/* --- État désactivé commun (conserve l'existant) --- */
.btn[disabled],
button[disabled] {
border-color: #ccc;
color: #666;
background-image: none;
}
@media (prefers-reduced-motion: no-preference) {
.btn {
transition: background-color 0.15s ease, color 0.15s ease,
border-color 0.15s ease;
}
}
- Step 4: Relancer le test de non-régression
Run: rtk pytest tests/test_boutons.py::test_btn_primary_is_terracotta -v
Expected: PASS (le .btn-primary conserve son dégradé terracotta et son texte blanc).
- Step 5: Vérifier l'absence de régression sur la suite complète
Run: rtk pytest
Expected: PASS (aucune régression introduite par le CSS).
- Step 6: Vérification visuelle des trois rôles
Lancer l'app (python run.py), se connecter, ouvrir /compte/vues. Vérifier :
-
« Renommer » : gris ardoise lisible (texte
#344054, bord#5a6570), plus de gris clair illisible ; -
« Supprimer » : rouge
#c0392ben outline, plus aucun mauve ; au survol, plein rouge texte blanc ; -
focus clavier (Tab) : anneau/fond visible sur chaque bouton. Prendre une capture pour comparaison avec la capture initiale.
-
Step 7: Commit (différé — voir note d'intégration en fin de plan : spec + plan + code committés ensemble)
Task 2: Audit et conformation des boutons existants
Passe en revue les ~46 occurrences color=… et 8 outline=True pour s'assurer qu'elles respectent la charte : une seule action primary pleine par contexte, destructif outline par défaut sauf confirmation finale en modale (plein rouge), neutre en secondary outline.
Files:
- Inspect:
src/saved_views/ui.py,src/pages/compte_vues.py,src/pages/compte_abonnement.py,src/pages/compte_abonnement_mes_infos.py,src/pages/mot_de_passe_oublie.py,src/pages/recherche.py,src/pages/tableau.py,src/app.py - Modify: uniquement les fichiers dont un bouton enfreint la charte (à déterminer pendant l'audit)
- Test:
tests/test_boutons.py(réutilise la garde de non-régression de Task 1)
Interfaces:
-
Consumes: classes CSS restylées de Task 1.
-
Produces: aucun symbole ; usages de boutons conformes à la charte.
-
Step 1: Recenser les boutons et leur rôle
Run: grep -rn 'dbc.Button\|className="btn' src/ --include=*.py
Pour chaque bouton, noter (page, libellé, color, outline) et confronter à la charte :
| Cas | Attendu |
|---|---|
| Action principale d'un écran/formulaire (Rechercher, Enregistrer, Envoyer, S'abonner) | color="primary" plein |
| Déclencheur destructif dans une liste (Supprimer, Se désabonner) | color="danger", outline=True |
| Confirmation destructive finale en modale | color="danger" plein |
| Action neutre (Renommer, Annuler, navigation) | color="secondary", outline=True |
Vérifier en particulier : aucun écran ne doit présenter deux boutons primary pleins en concurrence.
- Step 2: Corriger les boutons non conformes
Éditer uniquement les boutons divergents. Exemple de correction type (n'appliquer que si un cas réel est trouvé) :
# Avant — déclencheur de suppression en plein rouge dans une liste
dbc.Button("Supprimer", id={"type": "vue-delete", "index": vid}, color="danger")
# Après — outline par défaut hors confirmation finale
dbc.Button("Supprimer", id={"type": "vue-delete", "index": vid}, color="danger", outline=True)
Si l'audit ne révèle aucune divergence (les usages de src/saved_views/ui.py sont déjà conformes : Renommer = secondary+outline, Supprimer = danger+outline ; la confirmation modale vue-rename-confirm = primary), documenter « aucun changement nécessaire » dans le message de commit et passer au Step 3.
- Step 3: Vérifier la non-régression
Run: rtk pytest
Expected: PASS.
- Step 4: Vérification visuelle multi-pages
Lancer l'app, parcourir /compte/vues, /compte/abonnement, / et /tableau. Confirmer que chaque bouton respecte son rôle et qu'il n'y a pas deux primary pleins concurrents par écran.
- Step 5: Commit (différé — voir note d'intégration ci-dessous)
Note d'intégration
Sur demande explicite de l'utilisateur, le spec + le plan + le code sont committés ensemble (pas de commit intermédiaire). Après validation des deux tâches, faire un unique commit :
git add docs/superpowers/specs/2026-06-30-charte-boutons-design.md \
docs/superpowers/plans/2026-06-30-charte-boutons.md \
src/assets/css/style.css tests/test_boutons.py
# + tout fichier page modifié pendant l'audit (Task 2)
git commit -m "feat: charte graphique des boutons (primary/secondary/danger) #<issue>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
Branche courante : dev (auto-déploie sur test.decp.info). Ne pas pousser sans
demande explicite.