Files
colibre/docs/superpowers/plans/2026-06-30-charte-boutons.md
Colin Maudry 0cfafedeef feat: charte graphique des boutons (primary/secondary/danger)
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>
2026-06-30 13:21:24 +02:00

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: 3px et typographie Inter poids 400 conservés sur les boutons.
  • Focus clavier visible (:focus-visible) sur tous les boutons restylés (accessibilité).
  • Respecter prefers-reduced-motion pour toute transition de survol.
  • Tests : rtk pytest (Selenium, nécessite Chrome/Chromium). DEVELOPMENT=true est 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 #c0392b en 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.